diff --git a/CLAUDE.md b/CLAUDE.md index 33094d46..bd026474 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -554,7 +554,7 @@ Typed arrays: `let nums: array = [1, 2, 3];` --- -## Standard Library (42 modules) +## Standard Library (43 modules) Import with `@stdlib/` prefix: ```hemlock @@ -589,6 +589,7 @@ import { TcpStream, UdpSocket } from "@stdlib/net"; | `logging` | Logger with levels | | `math` | sin, cos, sqrt, pow, rand, PI, E | | `net` | TcpListener, TcpStream, UdpSocket | +| `onnx` | ONNX Runtime inference (local ML models) | | `os` | platform, arch, cpu_count, hostname | | `path` | File path manipulation | | `process` | fork, exec, wait, kill | @@ -979,7 +980,7 @@ make parity - Manual memory management with `talloc()` and `sizeof()` - Async/await with true pthread parallelism - Atomic operations for lock-free concurrent programming -- 42 stdlib modules (+ arena, assert, semver, toml, retry, iter, random, shell, termios, vector) +- 43 stdlib modules (+ arena, assert, semver, toml, retry, iter, random, shell, termios, vector, onnx) - FFI for C interop with `export extern fn` for reusable library wrappers - FFI struct support in compiler (pass C structs by value) - FFI pointer helpers (`ptr_null`, `ptr_read_*`, `ptr_write_*`) diff --git a/Makefile b/Makefile index e164906c..9b69ac82 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,14 @@ ifeq ($(shell uname),Darwin) else HAS_LIBWEBSOCKETS := 0 endif + + # On macOS, check for ONNX Runtime + BREW_ONNXRUNTIME := $(shell brew --prefix onnxruntime 2>/dev/null) + ifneq ($(BREW_ONNXRUNTIME),) + HAS_ONNXRUNTIME := $(shell test -f $(BREW_ONNXRUNTIME)/lib/libonnxruntime.dylib && echo 1 || echo 0) + else + HAS_ONNXRUNTIME := 0 + endif else # On Linux, use pkg-config if available LIBFFI_CFLAGS := $(shell pkg-config --cflags libffi 2>/dev/null) @@ -48,6 +56,9 @@ else # Check if libwebsockets is available on Linux HAS_LIBWEBSOCKETS := $(shell pkg-config --exists libwebsockets 2>/dev/null && echo 1 || (test -f /usr/include/libwebsockets.h && echo 1 || echo 0)) + + # Check if ONNX Runtime is available on Linux + HAS_ONNXRUNTIME := $(shell pkg-config --exists libonnxruntime 2>/dev/null && echo 1 || (test -f /usr/local/include/onnxruntime_c_api.h && echo 1 || (test -f /usr/include/onnxruntime/core/session/onnxruntime_c_api.h && echo 1 || echo 0))) endif # Base libraries (always required) @@ -177,6 +188,17 @@ else $(CC) -shared -fPIC -o stdlib/c/libffi_struct_test.so stdlib/c/ffi_struct_test.c endif @echo "✓ libffi_struct_test built successfully" +ifeq ($(HAS_ONNXRUNTIME),1) + @echo "Building stdlib/c/libhemlock_onnx..." +ifeq ($(shell uname),Darwin) + $(CC) -shared -fPIC -I$(BREW_ONNXRUNTIME)/include -o stdlib/c/libhemlock_onnx.dylib stdlib/c/onnx_wrapper.c -L$(BREW_ONNXRUNTIME)/lib -lonnxruntime +else + $(CC) -shared -fPIC -o stdlib/c/libhemlock_onnx.so stdlib/c/onnx_wrapper.c -lonnxruntime +endif + @echo "✓ libhemlock_onnx built successfully" +else + @echo "⊘ Skipping libhemlock_onnx (libonnxruntime not installed)" +endif # Clean stdlib builds .PHONY: stdlib-clean diff --git a/stdlib/c/README.md b/stdlib/c/README.md index 6ab0cd29..d2871d24 100644 --- a/stdlib/c/README.md +++ b/stdlib/c/README.md @@ -50,12 +50,45 @@ statically linked into the Hemlock interpreter. No separate FFI wrapper is neede The `lws_wrapper.so` is primarily used for WebSocket functionality. +## ONNX Runtime Wrapper + +### Purpose + +`onnx_wrapper.c` provides a flat C interface to ONNX Runtime's `OrtApi` struct-of-function-pointers, +enabling Hemlock's FFI system to call ONNX Runtime functions directly. + +### Requirements + +```bash +# From GitHub releases +wget https://github.com/microsoft/onnxruntime/releases/download/v1.17.0/onnxruntime-linux-x64-1.17.0.tgz +tar xzf onnxruntime-linux-x64-1.17.0.tgz +sudo cp lib/libonnxruntime.so* /usr/local/lib/ +sudo cp -r include/* /usr/local/include/ +sudo ldconfig + +# macOS +brew install onnxruntime +``` + +### Building + +```bash +# From hemlock root directory +make stdlib +``` + +This compiles `onnx_wrapper.c` → `libhemlock_onnx.so` + +### Usage + +Used by `@stdlib/onnx` (`stdlib/onnx.hml`) for local ML inference. + ## Future C Modules Planned FFI wrappers: - OpenSSL wrapper for crypto functions - zlib wrapper for compression -- SQLite wrapper for embedded database ## Design Philosophy diff --git a/stdlib/c/onnx_wrapper.c b/stdlib/c/onnx_wrapper.c new file mode 100644 index 00000000..32bc9658 --- /dev/null +++ b/stdlib/c/onnx_wrapper.c @@ -0,0 +1,481 @@ +/* + * onnx_wrapper.c - Flat C wrapper for ONNX Runtime's OrtApi struct + * + * ONNX Runtime uses a struct-of-function-pointers pattern (OrtApi) accessed + * via OrtGetApiBase(). This wrapper flattens that into simple exported C + * functions that Hemlock can call through its FFI system. + * + * Build: + * gcc -shared -fPIC -o libhemlock_onnx.so onnx_wrapper.c -lonnxruntime + * + * Requires: + * - libonnxruntime.so (ONNX Runtime shared library) + * - onnxruntime_c_api.h (ONNX Runtime C API header) + * + * Install ONNX Runtime: + * - From GitHub releases: https://github.com/microsoft/onnxruntime/releases + * - Extract and copy: + * sudo cp lib/libonnxruntime.so* /usr/local/lib/ + * sudo cp include/* /usr/local/include/ + * sudo ldconfig + */ + +#include +#include +#include +#include + +/* ========================================================================== */ +/* Internal State */ +/* ========================================================================== */ + +static const OrtApi* g_ort = NULL; + +static int ensure_api(void) { + if (g_ort) return 0; + const OrtApiBase* base = OrtGetApiBase(); + if (!base) return -1; + g_ort = base->GetApi(ORT_API_VERSION); + return g_ort ? 0 : -1; +} + +/* + * Error handling pattern (matches Hemlock's vector module): + * - Functions take a char** err parameter + * - On error, *err is set to a strdup'd error message + * - On success, *err is left unchanged (caller should init to NULL) + * - Caller is responsible for freeing *err + */ +static int check_status(OrtStatus* status, char** err) { + if (status == NULL) return 0; /* success */ + if (g_ort && err) { + const char* msg = g_ort->GetErrorMessage(status); + *err = msg ? strdup(msg) : strdup("unknown ONNX Runtime error"); + } + if (g_ort) g_ort->ReleaseStatus(status); + return -1; +} + +/* ========================================================================== */ +/* Version & Initialization */ +/* ========================================================================== */ + +/* Returns the ONNX Runtime version string (e.g., "1.17.0") */ +const char* hml_ort_version(void) { + const OrtApiBase* base = OrtGetApiBase(); + if (!base) return "unknown"; + return base->GetVersionString(); +} + +/* Returns the ORT API version number this wrapper was built against */ +int hml_ort_api_version(void) { + return ORT_API_VERSION; +} + +/* ========================================================================== */ +/* Environment */ +/* ========================================================================== */ + +/* Create an ORT environment + * log_level: 0=verbose, 1=info, 2=warning, 3=error, 4=fatal + * logid: identifier string for logging + * err: error output + * Returns: OrtEnv* or NULL on error + */ +OrtEnv* hml_ort_create_env(int log_level, const char* logid, char** err) { + if (ensure_api() != 0) { + if (err) *err = strdup("failed to initialize ONNX Runtime API"); + return NULL; + } + OrtEnv* env = NULL; + if (check_status(g_ort->CreateEnv((OrtLoggingLevel)log_level, logid, &env), err)) { + return NULL; + } + return env; +} + +void hml_ort_release_env(OrtEnv* env) { + if (g_ort && env) g_ort->ReleaseEnv(env); +} + +/* ========================================================================== */ +/* Session Options */ +/* ========================================================================== */ + +OrtSessionOptions* hml_ort_create_session_options(char** err) { + if (ensure_api() != 0) { + if (err) *err = strdup("failed to initialize ONNX Runtime API"); + return NULL; + } + OrtSessionOptions* opts = NULL; + if (check_status(g_ort->CreateSessionOptions(&opts), err)) { + return NULL; + } + return opts; +} + +void hml_ort_release_session_options(OrtSessionOptions* opts) { + if (g_ort && opts) g_ort->ReleaseSessionOptions(opts); +} + +int hml_ort_session_options_set_intra_threads(OrtSessionOptions* opts, int num, char** err) { + if (!g_ort || !opts) return -1; + return check_status(g_ort->SetIntraOpNumThreads(opts, num), err); +} + +int hml_ort_session_options_set_inter_threads(OrtSessionOptions* opts, int num, char** err) { + if (!g_ort || !opts) return -1; + return check_status(g_ort->SetInterOpNumThreads(opts, num), err); +} + +/* Graph optimization level: 0=disable, 1=basic, 2=extended, 99=all */ +int hml_ort_session_options_set_graph_opt(OrtSessionOptions* opts, int level, char** err) { + if (!g_ort || !opts) return -1; + return check_status( + g_ort->SetSessionGraphOptimizationLevel(opts, (GraphOptimizationLevel)level), err); +} + +/* ========================================================================== */ +/* Session */ +/* ========================================================================== */ + +/* Create a session from a model file path */ +OrtSession* hml_ort_create_session(OrtEnv* env, const char* model_path, + OrtSessionOptions* opts, char** err) { + if (!g_ort || !env) { + if (err) *err = strdup("invalid environment"); + return NULL; + } + OrtSession* session = NULL; + if (check_status(g_ort->CreateSession(env, model_path, opts, &session), err)) { + return NULL; + } + return session; +} + +/* Create a session from a model buffer in memory */ +OrtSession* hml_ort_create_session_from_array(OrtEnv* env, const void* model_data, + size_t model_data_len, + OrtSessionOptions* opts, char** err) { + if (!g_ort || !env) { + if (err) *err = strdup("invalid environment"); + return NULL; + } + OrtSession* session = NULL; + if (check_status( + g_ort->CreateSessionFromArray(env, model_data, model_data_len, opts, &session), err)) { + return NULL; + } + return session; +} + +void hml_ort_release_session(OrtSession* session) { + if (g_ort && session) g_ort->ReleaseSession(session); +} + +/* ========================================================================== */ +/* Model Inspection */ +/* ========================================================================== */ + +/* Get number of model inputs */ +size_t hml_ort_session_input_count(OrtSession* session, char** err) { + if (!g_ort || !session) return 0; + size_t count = 0; + check_status(g_ort->SessionGetInputCount(session, &count), err); + return count; +} + +/* Get number of model outputs */ +size_t hml_ort_session_output_count(OrtSession* session, char** err) { + if (!g_ort || !session) return 0; + size_t count = 0; + check_status(g_ort->SessionGetOutputCount(session, &count), err); + return count; +} + +/* Get input name by index. Returns malloc'd string; caller must call hml_ort_free_name(). */ +char* hml_ort_session_input_name(OrtSession* session, size_t idx, char** err) { + if (!g_ort || !session) return NULL; + OrtAllocator* allocator = NULL; + if (check_status(g_ort->GetAllocatorWithDefaultOptions(&allocator), err)) return NULL; + char* name = NULL; + if (check_status(g_ort->SessionGetInputName(session, idx, allocator, &name), err)) return NULL; + /* Copy to a standard malloc'd string so we can free with free() */ + char* copy = strdup(name); + g_ort->AllocatorFree(allocator, name); + return copy; +} + +/* Get output name by index. Returns malloc'd string; caller must call hml_ort_free_name(). */ +char* hml_ort_session_output_name(OrtSession* session, size_t idx, char** err) { + if (!g_ort || !session) return NULL; + OrtAllocator* allocator = NULL; + if (check_status(g_ort->GetAllocatorWithDefaultOptions(&allocator), err)) return NULL; + char* name = NULL; + if (check_status(g_ort->SessionGetOutputName(session, idx, allocator, &name), err)) return NULL; + char* copy = strdup(name); + g_ort->AllocatorFree(allocator, name); + return copy; +} + +void hml_ort_free_name(char* name) { + free(name); +} + +/* Get input tensor shape. Writes dimensions to shape_out, returns number of dims. + * shape_out must be pre-allocated with at least max_dims entries. + * Returns -1 on error. */ +int64_t hml_ort_session_input_shape(OrtSession* session, size_t idx, + int64_t* shape_out, size_t max_dims, char** err) { + if (!g_ort || !session) return -1; + OrtTypeInfo* type_info = NULL; + if (check_status(g_ort->SessionGetInputTypeInfo(session, idx, &type_info), err)) return -1; + + const OrtTensorTypeAndShapeInfo* tensor_info = NULL; + if (check_status(g_ort->CastTypeInfoToTensorInfo(type_info, &tensor_info), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + size_t ndims = 0; + if (check_status(g_ort->GetDimensionsCount(tensor_info, &ndims), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + if (ndims > max_dims) ndims = max_dims; + if (check_status(g_ort->GetDimensions(tensor_info, shape_out, ndims), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + g_ort->ReleaseTypeInfo(type_info); + return (int64_t)ndims; +} + +/* Get output tensor shape */ +int64_t hml_ort_session_output_shape(OrtSession* session, size_t idx, + int64_t* shape_out, size_t max_dims, char** err) { + if (!g_ort || !session) return -1; + OrtTypeInfo* type_info = NULL; + if (check_status(g_ort->SessionGetOutputTypeInfo(session, idx, &type_info), err)) return -1; + + const OrtTensorTypeAndShapeInfo* tensor_info = NULL; + if (check_status(g_ort->CastTypeInfoToTensorInfo(type_info, &tensor_info), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + size_t ndims = 0; + if (check_status(g_ort->GetDimensionsCount(tensor_info, &ndims), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + if (ndims > max_dims) ndims = max_dims; + if (check_status(g_ort->GetDimensions(tensor_info, shape_out, ndims), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + g_ort->ReleaseTypeInfo(type_info); + return (int64_t)ndims; +} + +/* Get input element type (returns ONNXTensorElementDataType enum value) */ +int hml_ort_session_input_type(OrtSession* session, size_t idx, char** err) { + if (!g_ort || !session) return -1; + OrtTypeInfo* type_info = NULL; + if (check_status(g_ort->SessionGetInputTypeInfo(session, idx, &type_info), err)) return -1; + + const OrtTensorTypeAndShapeInfo* tensor_info = NULL; + if (check_status(g_ort->CastTypeInfoToTensorInfo(type_info, &tensor_info), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + ONNXTensorElementDataType type = ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED; + if (check_status(g_ort->GetTensorElementType(tensor_info, &type), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + g_ort->ReleaseTypeInfo(type_info); + return (int)type; +} + +/* Get output element type */ +int hml_ort_session_output_type(OrtSession* session, size_t idx, char** err) { + if (!g_ort || !session) return -1; + OrtTypeInfo* type_info = NULL; + if (check_status(g_ort->SessionGetOutputTypeInfo(session, idx, &type_info), err)) return -1; + + const OrtTensorTypeAndShapeInfo* tensor_info = NULL; + if (check_status(g_ort->CastTypeInfoToTensorInfo(type_info, &tensor_info), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + ONNXTensorElementDataType type = ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED; + if (check_status(g_ort->GetTensorElementType(tensor_info, &type), err)) { + g_ort->ReleaseTypeInfo(type_info); + return -1; + } + + g_ort->ReleaseTypeInfo(type_info); + return (int)type; +} + +/* ========================================================================== */ +/* Memory Info */ +/* ========================================================================== */ + +OrtMemoryInfo* hml_ort_create_cpu_memory_info(char** err) { + if (ensure_api() != 0) { + if (err) *err = strdup("failed to initialize ONNX Runtime API"); + return NULL; + } + OrtMemoryInfo* info = NULL; + if (check_status( + g_ort->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &info), err)) { + return NULL; + } + return info; +} + +void hml_ort_release_memory_info(OrtMemoryInfo* info) { + if (g_ort && info) g_ort->ReleaseMemoryInfo(info); +} + +/* ========================================================================== */ +/* Tensor Creation */ +/* ========================================================================== */ + +/* + * Create a tensor wrapping user-provided data. + * The data buffer must remain valid for the lifetime of the tensor. + * + * data: pointer to contiguous element data + * data_len: size of data in bytes + * shape: pointer to int64_t array of dimension sizes + * ndims: number of dimensions + * element_type: ONNXTensorElementDataType enum value + * mem_info: CPU memory info (from hml_ort_create_cpu_memory_info) + * err: error output + */ +OrtValue* hml_ort_create_tensor(void* data, size_t data_len, + const int64_t* shape, size_t ndims, + int element_type, + OrtMemoryInfo* mem_info, char** err) { + if (!g_ort || !mem_info) { + if (err) *err = strdup("invalid memory info or uninitialized API"); + return NULL; + } + OrtValue* value = NULL; + if (check_status( + g_ort->CreateTensorWithDataAsOrtValue( + mem_info, data, data_len, shape, ndims, + (ONNXTensorElementDataType)element_type, &value), err)) { + return NULL; + } + return value; +} + +void hml_ort_release_value(OrtValue* value) { + if (g_ort && value) g_ort->ReleaseValue(value); +} + +/* ========================================================================== */ +/* Tensor Inspection */ +/* ========================================================================== */ + +/* Get pointer to raw tensor data */ +void* hml_ort_tensor_data(OrtValue* value, char** err) { + if (!g_ort || !value) return NULL; + void* data = NULL; + check_status(g_ort->GetTensorMutableData(value, &data), err); + return data; +} + +/* Get number of dimensions */ +int64_t hml_ort_tensor_ndims(OrtValue* value, char** err) { + if (!g_ort || !value) return -1; + OrtTensorTypeAndShapeInfo* info = NULL; + if (check_status(g_ort->GetTensorTypeAndShape(value, &info), err)) return -1; + size_t ndims = 0; + int rc = check_status(g_ort->GetDimensionsCount(info, &ndims), err); + g_ort->ReleaseTensorTypeAndShapeInfo(info); + return rc ? -1 : (int64_t)ndims; +} + +/* Get tensor shape. shape_out must be pre-allocated. Returns 0 on success. */ +int hml_ort_tensor_shape(OrtValue* value, int64_t* shape_out, size_t max_dims, char** err) { + if (!g_ort || !value) return -1; + OrtTensorTypeAndShapeInfo* info = NULL; + if (check_status(g_ort->GetTensorTypeAndShape(value, &info), err)) return -1; + + size_t ndims = 0; + if (check_status(g_ort->GetDimensionsCount(info, &ndims), err)) { + g_ort->ReleaseTensorTypeAndShapeInfo(info); + return -1; + } + if (ndims > max_dims) ndims = max_dims; + int rc = check_status(g_ort->GetDimensions(info, shape_out, ndims), err); + g_ort->ReleaseTensorTypeAndShapeInfo(info); + return rc; +} + +/* Get total number of elements in tensor */ +int64_t hml_ort_tensor_element_count(OrtValue* value, char** err) { + if (!g_ort || !value) return -1; + OrtTensorTypeAndShapeInfo* info = NULL; + if (check_status(g_ort->GetTensorTypeAndShape(value, &info), err)) return -1; + size_t count = 0; + int rc = check_status(g_ort->GetTensorElementCount(info, &count), err); + g_ort->ReleaseTensorTypeAndShapeInfo(info); + return rc ? -1 : (int64_t)count; +} + +/* Get tensor element type (returns ONNXTensorElementDataType) */ +int hml_ort_tensor_element_type(OrtValue* value, char** err) { + if (!g_ort || !value) return -1; + OrtTensorTypeAndShapeInfo* info = NULL; + if (check_status(g_ort->GetTensorTypeAndShape(value, &info), err)) return -1; + ONNXTensorElementDataType type = ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED; + int rc = check_status(g_ort->GetTensorElementType(info, &type), err); + g_ort->ReleaseTensorTypeAndShapeInfo(info); + return rc ? -1 : (int)type; +} + +/* ========================================================================== */ +/* Inference */ +/* ========================================================================== */ + +/* + * Run inference. + * + * session: ORT session + * input_names: array of C strings (input tensor names) + * input_values: array of OrtValue* (input tensors) + * num_inputs: number of inputs + * output_names: array of C strings (output tensor names) + * num_outputs: number of outputs + * output_values: pre-allocated array of OrtValue* to receive outputs + * err: error output + * + * Returns 0 on success, -1 on error. + * On success, output_values[] is filled with OrtValue* pointers owned by the caller. + */ +int hml_ort_run(OrtSession* session, + const char* const* input_names, const OrtValue* const* input_values, + size_t num_inputs, + const char* const* output_names, size_t num_outputs, + OrtValue** output_values, char** err) { + if (!g_ort || !session) { + if (err) *err = strdup("invalid session"); + return -1; + } + return check_status( + g_ort->Run(session, NULL, input_names, input_values, num_inputs, + output_names, num_outputs, output_values), err); +} diff --git a/stdlib/docs/onnx.md b/stdlib/docs/onnx.md new file mode 100644 index 00000000..c2b25c85 --- /dev/null +++ b/stdlib/docs/onnx.md @@ -0,0 +1,622 @@ +# Hemlock ONNX Runtime Module + +A standard library module for running ONNX models locally via ONNX Runtime FFI. Enables local embeddings, classification, and general ML inference without API calls. + +## Overview + +The ONNX module provides: + +- **Session management** — Load ONNX models, configure threading and optimization +- **Tensor operations** — Create input tensors, read output tensors (f32, f64, i32, i64) +- **Inference** — Run models with named or positional inputs +- **Model inspection** — Query input/output names, shapes, and types +- **Resource management** — Explicit cleanup of sessions and tensors + +## Usage + +```hemlock +import { create_session, run, create_tensor_f32, tensor_data_f32, + free_tensor, free_session } from "@stdlib/onnx"; + +// Load an ONNX model +let session = create_session("model.onnx"); + +// Create an input tensor +let input = create_tensor_f32([1.0, 2.0, 3.0, 4.0], [1, 4]); + +// Run inference +let outputs = run(session, { "input": input }); + +// Read results +let result = tensor_data_f32(outputs[0]); +print(result); // e.g., [0.8, 0.1, 0.05, 0.05] + +// Cleanup +free_tensor(outputs[0]); +free_tensor(input); +free_session(session); +``` + +Or import all: + +```hemlock +import * as onnx from "@stdlib/onnx"; + +let session = onnx.create_session("model.onnx"); +// ... +onnx.free_session(session); +``` + +--- + +## Session Lifecycle + +### create_session(model_path, log_level?, intra_threads?, inter_threads?, optimization?) + +Loads an ONNX model and creates an inference session. + +**Parameters:** +- `model_path: string` — Path to the `.onnx` model file (required) +- `log_level: i32` — Logging verbosity (default: `LOG_WARNING`) +- `intra_threads: i32` — Intra-op parallelism threads (default: `0` = auto) +- `inter_threads: i32` — Inter-op parallelism threads (default: `0` = auto) +- `optimization: i32` — Graph optimization level (default: `OPT_ALL`) + +**Returns:** `Session` + +**Throws:** On invalid model path, corrupt model, or initialization failure + +```hemlock +import { create_session, free_session, + LOG_ERROR, OPT_BASIC } from "@stdlib/onnx"; + +// Simple — sane defaults +let session = create_session("model.onnx"); + +// Full control +let session2 = create_session("model.onnx", + log_level: LOG_ERROR, + intra_threads: 4, + inter_threads: 2, + optimization: OPT_BASIC); + +free_session(session); +free_session(session2); +``` + +### free_session(session) + +Releases all resources (environment, session, options, memory info). + +**Parameters:** +- `session: Session` — Session to free + +**Returns:** `null` + +Calling `free_session` on an already-freed session is safe (no-op). + +--- + +## Model Inspection + +### input_count(session) / output_count(session) + +Get the number of model inputs/outputs. + +**Returns:** `i64` + +### input_names(session) / output_names(session) + +Get all input/output names as an array of strings. + +**Returns:** `array` of `string` + +### input_shape(session, idx) / output_shape(session, idx) + +Get the shape of an input/output tensor by index. + +**Parameters:** +- `session: Session` +- `idx: i64` — 0-based index + +**Returns:** `array` of `i64` — Dimension sizes (dynamic dimensions may be `-1`) + +### input_type(session, idx) / output_type(session, idx) + +Get the element type of an input/output tensor. + +**Returns:** `i32` — One of the `DTYPE_*` constants + +### describe(session) + +Print a human-readable summary of the model's inputs and outputs. + +```hemlock +import { create_session, describe, free_session } from "@stdlib/onnx"; + +let session = create_session("model.onnx"); +describe(session); +// Output: +// Model inputs (1): +// [0] input: float32 [1, 4] +// Model outputs (1): +// [0] output: float32 [1, 2] +free_session(session); +``` + +--- + +## Tensor Creation + +### create_tensor_f32(data, shape) + +Creates a float32 tensor from a flat data array and shape. + +**Parameters:** +- `data: array` — Flat array of numeric values +- `shape: array` — Array of dimension sizes (e.g., `[1, 3, 224, 224]`) + +**Returns:** `Tensor` + +**Throws:** If `data.length` doesn't match the product of `shape` dimensions + +```hemlock +import { create_tensor_f32, free_tensor } from "@stdlib/onnx"; + +// 1D tensor +let t1 = create_tensor_f32([1.0, 2.0, 3.0], [3]); + +// 2D tensor (batch of 1, 4 features) +let t2 = create_tensor_f32([1.0, 2.0, 3.0, 4.0], [1, 4]); + +// 4D tensor (batch=1, channels=3, height=2, width=2) +let t3 = create_tensor_f32( + [0.1, 0.2, 0.3, 0.4, + 0.5, 0.6, 0.7, 0.8, + 0.9, 1.0, 1.1, 1.2], + [1, 3, 2, 2]); + +free_tensor(t1); +free_tensor(t2); +free_tensor(t3); +``` + +### create_tensor_f64(data, shape) + +Creates a float64 (double) tensor. Same interface as `create_tensor_f32`. + +### create_tensor_i32(data, shape) + +Creates an int32 tensor. Same interface. + +### create_tensor_i64(data, shape) + +Creates an int64 tensor. Same interface. + +```hemlock +import { create_tensor_i64, free_tensor } from "@stdlib/onnx"; + +// Token IDs for a language model +let input_ids = create_tensor_i64([101, 2054, 2003, 1996, 3007, 102], [1, 6]); +free_tensor(input_ids); +``` + +### free_tensor(tensor) + +Frees tensor resources. Safe to call multiple times (no-op after first). + +--- + +## Tensor Inspection + +### tensor_shape(tensor) + +Get the tensor's shape. + +**Returns:** `array` of `i64` + +### tensor_element_count(tensor) + +Get the total number of elements. + +**Returns:** `i64` + +### tensor_type(tensor) + +Get the element data type. + +**Returns:** `i32` — One of the `DTYPE_*` constants + +### tensor_data_f32(tensor) / tensor_data_f64(tensor) + +Read tensor data as an array of float values. + +**Returns:** `array` + +### tensor_data_i32(tensor) / tensor_data_i64(tensor) + +Read tensor data as an array of integer values. + +**Returns:** `array` + +```hemlock +import { create_tensor_f32, tensor_shape, tensor_element_count, + tensor_type, tensor_data_f32, free_tensor, DTYPE_FLOAT } from "@stdlib/onnx"; + +let t = create_tensor_f32([1.0, 2.0, 3.0, 4.0], [2, 2]); +print(tensor_shape(t)); // [2, 2] +print(tensor_element_count(t)); // 4 +print(tensor_type(t)); // 1 (DTYPE_FLOAT) +print(tensor_data_f32(t)); // [1.0, 2.0, 3.0, 4.0] +free_tensor(t); +``` + +--- + +## Inference + +### run(session, inputs) + +Run model inference. + +**Parameters:** +- `session: Session` — Loaded model session +- `inputs` — Either: + - **Object** mapping input names to Tensors: `{ "input": tensor }` + - **Array** of Tensors in model input order: `[tensor]` + +**Returns:** `array` of `Tensor` — Output tensors (caller must `free_tensor()` each) + +**Throws:** On inference error, missing inputs, or input count mismatch + +```hemlock +import { create_session, run, create_tensor_f32, tensor_data_f32, + free_tensor, free_session } from "@stdlib/onnx"; + +let session = create_session("classifier.onnx"); + +let features = create_tensor_f32([5.1, 3.5, 1.4, 0.2], [1, 4]); + +// Named inputs (recommended) +let outputs = run(session, { "input": features }); + +// Positional inputs (alternative) +let outputs2 = run(session, [features]); + +let probabilities = tensor_data_f32(outputs[0]); +print("Class probabilities:", probabilities); + +free_tensor(outputs[0]); +free_tensor(outputs2[0]); +free_tensor(features); +free_session(session); +``` + +--- + +## Utility Functions + +### version() + +Get the ONNX Runtime library version string. + +**Returns:** `string` (e.g., `"1.17.0"`) + +### api_version() + +Get the ORT API version this wrapper was built against. + +**Returns:** `i32` + +### dtype_name(dtype) + +Get a human-readable name for a `DTYPE_*` constant. + +**Returns:** `string` (e.g., `"float32"`, `"int64"`) + +```hemlock +import { version, api_version, dtype_name, DTYPE_FLOAT } from "@stdlib/onnx"; + +print("ONNX Runtime:", version()); +print("API version:", api_version()); +print(dtype_name(DTYPE_FLOAT)); // "float32" +``` + +--- + +## Constants + +### Logging Levels + +```hemlock +LOG_VERBOSE // 0 - All messages +LOG_INFO // 1 - Info and above +LOG_WARNING // 2 - Warnings and above (default) +LOG_ERROR // 3 - Errors only +LOG_FATAL // 4 - Fatal errors only +``` + +### Graph Optimization Levels + +```hemlock +OPT_DISABLE // 0 - No optimization +OPT_BASIC // 1 - Constant folding, redundant node elimination +OPT_EXTENDED // 2 - Node fusions, etc. +OPT_ALL // 99 - All available optimizations (default) +``` + +### Tensor Element Types + +```hemlock +DTYPE_UNDEFINED // 0 +DTYPE_FLOAT // 1 - 32-bit float +DTYPE_UINT8 // 2 - 8-bit unsigned int +DTYPE_INT8 // 3 - 8-bit signed int +DTYPE_UINT16 // 4 - 16-bit unsigned int +DTYPE_INT16 // 5 - 16-bit signed int +DTYPE_INT32 // 6 - 32-bit signed int +DTYPE_INT64 // 7 - 64-bit signed int +DTYPE_STRING // 8 - String +DTYPE_BOOL // 9 - Boolean +DTYPE_FLOAT16 // 10 - IEEE half precision +DTYPE_DOUBLE // 11 - 64-bit double +DTYPE_UINT32 // 12 - 32-bit unsigned int +DTYPE_UINT64 // 13 - 64-bit unsigned int +``` + +--- + +## Examples + +### Image Classification + +```hemlock +import { create_session, run, create_tensor_f32, tensor_data_f32, + free_tensor, free_session } from "@stdlib/onnx"; +import { read_file } from "@stdlib/fs"; + +// Load a pre-trained image classifier (e.g., MobileNet) +let session = create_session("mobilenet_v2.onnx", intra_threads: 4); + +// Prepare input (1x3x224x224 normalized image tensor) +// In practice, you'd load and preprocess an image here +let pixels = []; +let i = 0; +while (i < 1 * 3 * 224 * 224) { + pixels.push(0.5); // placeholder + i = i + 1; +} +let input = create_tensor_f32(pixels, [1, 3, 224, 224]); + +// Run inference +let outputs = run(session, [input]); +let logits = tensor_data_f32(outputs[0]); + +// Find argmax +let max_idx = 0; +let max_val = logits[0]; +i = 1; +while (i < logits.length) { + if (logits[i] > max_val) { + max_val = logits[i]; + max_idx = i; + } + i = i + 1; +} +print("Predicted class:", max_idx, "score:", max_val); + +free_tensor(outputs[0]); +free_tensor(input); +free_session(session); +``` + +### Text Embeddings + +```hemlock +import { create_session, run, create_tensor_i64, tensor_data_f32, + free_tensor, free_session } from "@stdlib/onnx"; + +// Load a sentence embedding model (e.g., all-MiniLM-L6-v2) +let session = create_session("embedding_model.onnx"); + +// Tokenized input (from a tokenizer) +let input_ids = create_tensor_i64([101, 2054, 2003, 1996, 3007, 102], [1, 6]); +let attention_mask = create_tensor_i64([1, 1, 1, 1, 1, 1], [1, 6]); + +// Run inference +let outputs = run(session, { + "input_ids": input_ids, + "attention_mask": attention_mask, +}); + +// Get embedding vector +let embedding = tensor_data_f32(outputs[0]); +print("Embedding dimensions:", embedding.length); +print("First 5 values:", embedding[0], embedding[1], embedding[2], embedding[3], embedding[4]); + +// Cleanup +let i = 0; +while (i < outputs.length) { + free_tensor(outputs[i]); + i = i + 1; +} +free_tensor(input_ids); +free_tensor(attention_mask); +free_session(session); +``` + +### Model Inspection + +```hemlock +import { create_session, describe, input_count, output_count, + input_names, output_names, input_shape, output_shape, + input_type, dtype_name, free_session } from "@stdlib/onnx"; + +let session = create_session("model.onnx"); + +// Quick overview +describe(session); + +// Detailed inspection +let in_names = input_names(session); +let i = 0; +while (i < in_names.length) { + let shape = input_shape(session, i); + let dtype = input_type(session, i); + print("Input '" + in_names[i] + "': " + dtype_name(dtype) + " " + shape); + i = i + 1; +} + +free_session(session); +``` + +--- + +## Error Handling + +All functions throw exceptions on errors: + +```hemlock +import { create_session, run, create_tensor_f32, + free_tensor, free_session } from "@stdlib/onnx"; + +// Model loading errors +try { + let session = create_session("nonexistent.onnx"); +} catch (e) { + print("Load error:", e); +} + +// Inference errors +try { + let session = create_session("model.onnx"); + // Wrong shape + let bad_input = create_tensor_f32([1.0], [1, 1]); + let outputs = run(session, [bad_input]); +} catch (e) { + print("Inference error:", e); +} + +// Data/shape mismatch +try { + let t = create_tensor_f32([1.0, 2.0, 3.0], [2, 2]); // 3 != 2*2 +} catch (e) { + print("Tensor error:", e); +} +``` + +--- + +## Memory Management + +Hemlock's explicit memory management applies to ONNX tensors and sessions: + +1. **Always free sessions** with `free_session()` when done +2. **Always free tensors** with `free_tensor()` — both input and output tensors +3. **Output tensors from `run()` are owned by the caller** — you must free each one +4. **Input tensor data must remain valid** until after `run()` completes +5. **Double-free is safe** — calling `free_tensor()` or `free_session()` twice is a no-op + +```hemlock +import { create_session, run, create_tensor_f32, + free_tensor, free_session } from "@stdlib/onnx"; + +let session = create_session("model.onnx"); +let input = create_tensor_f32([1.0, 2.0, 3.0, 4.0], [1, 4]); + +let outputs = run(session, [input]); + +// Process outputs... +let result = tensor_data_f32(outputs[0]); + +// Free everything +let i = 0; +while (i < outputs.length) { + free_tensor(outputs[i]); + i = i + 1; +} +free_tensor(input); +free_session(session); +``` + +--- + +## Performance Tips + +1. **Reuse sessions** — Creating a session loads and optimizes the model. Create once, run many times. +2. **Set thread counts** — Use `intra_threads` and `inter_threads` for CPU parallelism. +3. **Use `OPT_ALL`** — Graph optimization (default) can significantly speed up inference. +4. **Batch inputs** — Use batch dimension > 1 when processing multiple inputs. +5. **Match tensor types** — Use the correct `create_tensor_*` for the model's expected input type. + +--- + +## Architecture + +The module uses a two-layer architecture: + +``` +Hemlock program + ↓ +@stdlib/onnx (onnx.hml) ← Pure Hemlock: high-level API + ↓ FFI +libhemlock_onnx.so ← C wrapper: flattens OrtApi struct + ↓ +libonnxruntime.so ← ONNX Runtime: ML inference engine +``` + +The C wrapper (`stdlib/c/onnx_wrapper.c`) is needed because ONNX Runtime's C API uses a struct-of-function-pointers pattern (`OrtApi`) that can't be called directly through Hemlock's FFI. The wrapper exposes flat C functions that Hemlock can bind via `extern fn`. + +--- + +## System Requirements + +- **ONNX Runtime** shared library (`libonnxruntime.so`) +- **Hemlock ONNX wrapper** (`libhemlock_onnx.so`) + +### Installing ONNX Runtime + +**From GitHub releases (recommended):** +```bash +# Download the latest release +wget https://github.com/microsoft/onnxruntime/releases/download/v1.17.0/onnxruntime-linux-x64-1.17.0.tgz +tar xzf onnxruntime-linux-x64-1.17.0.tgz +cd onnxruntime-linux-x64-1.17.0 + +# Install +sudo cp lib/libonnxruntime.so* /usr/local/lib/ +sudo cp -r include/* /usr/local/include/ +sudo ldconfig +``` + +**On macOS:** +```bash +brew install onnxruntime +``` + +### Building the wrapper + +```bash +# From hemlock root directory +make stdlib + +# Or manually: +gcc -shared -fPIC -o stdlib/c/libhemlock_onnx.so stdlib/c/onnx_wrapper.c -lonnxruntime +``` + +--- + +## See Also + +- [ONNX Runtime Documentation](https://onnxruntime.ai/docs/) +- [ONNX Runtime GitHub](https://github.com/microsoft/onnxruntime) +- [ONNX Model Zoo](https://github.com/onnx/models) — Pre-trained models +- `@stdlib/vector` — Vector similarity search (pair with embeddings) +- `@stdlib/http` — Fetch models or call APIs +- `@stdlib/json` — Parse model metadata + +--- + +## License + +Part of the Hemlock standard library. ONNX Runtime is licensed under MIT. diff --git a/stdlib/onnx.hml b/stdlib/onnx.hml new file mode 100644 index 00000000..f1057acf --- /dev/null +++ b/stdlib/onnx.hml @@ -0,0 +1,1079 @@ +// @stdlib/onnx - ONNX Runtime inference via FFI +// +// Run ONNX models locally for embeddings, classification, and general inference +// without API calls. Uses libonnxruntime via a thin C wrapper. +// +// System Requirements: +// - ONNX Runtime (libonnxruntime.so) +// Install: https://github.com/microsoft/onnxruntime/releases +// - Hemlock ONNX wrapper (libhemlock_onnx.so) +// Build: make stdlib (or manually: see stdlib/c/onnx_wrapper.c) +// +// Usage: +// import { create_session, run, create_tensor_f32, free_session } from "@stdlib/onnx"; +// +// let session = create_session("model.onnx"); +// let input = create_tensor_f32([1.0, 2.0, 3.0, 4.0], [1, 4]); +// let outputs = run(session, { "input": input }); +// let result = tensor_data_f32(outputs[0]); +// print(result); +// free_tensor(outputs[0]); +// free_session(session); + +// Import the C wrapper library +import "libhemlock_onnx.so"; + +// ============================================================================ +// Constants: Logging Levels +// ============================================================================ + +export let LOG_VERBOSE = 0; +export let LOG_INFO = 1; +export let LOG_WARNING = 2; +export let LOG_ERROR = 3; +export let LOG_FATAL = 4; + +// ============================================================================ +// Constants: Graph Optimization Levels +// ============================================================================ + +export let OPT_DISABLE = 0; // No graph optimization +export let OPT_BASIC = 1; // Basic optimizations (constant folding, redundant node elimination) +export let OPT_EXTENDED = 2; // Extended optimizations (node fusions, etc.) +export let OPT_ALL = 99; // All available optimizations + +// ============================================================================ +// Constants: Tensor Element Data Types (ONNXTensorElementDataType) +// ============================================================================ + +export let DTYPE_UNDEFINED = 0; +export let DTYPE_FLOAT = 1; // 32-bit float +export let DTYPE_UINT8 = 2; // 8-bit unsigned integer +export let DTYPE_INT8 = 3; // 8-bit signed integer +export let DTYPE_UINT16 = 4; // 16-bit unsigned integer +export let DTYPE_INT16 = 5; // 16-bit signed integer +export let DTYPE_INT32 = 6; // 32-bit signed integer +export let DTYPE_INT64 = 7; // 64-bit signed integer +export let DTYPE_STRING = 8; // String +export let DTYPE_BOOL = 9; // Boolean +export let DTYPE_FLOAT16 = 10; // IEEE 16-bit float +export let DTYPE_DOUBLE = 11; // 64-bit double +export let DTYPE_UINT32 = 12; // 32-bit unsigned integer +export let DTYPE_UINT64 = 13; // 64-bit unsigned integer + +// ============================================================================ +// Size Constants +// ============================================================================ + +let SIZEOF_F32 = 4; +let SIZEOF_F64 = 8; +let SIZEOF_I32 = 4; +let SIZEOF_I64 = 8; +let SIZEOF_PTR = 8; + +// ============================================================================ +// FFI Bindings (flat C wrapper functions) +// ============================================================================ + +// Version +extern fn hml_ort_version(): ptr; +extern fn hml_ort_api_version(): i32; + +// Environment +extern fn hml_ort_create_env(log_level: i32, logid: ptr, err: ptr): ptr; +extern fn hml_ort_release_env(env: ptr): void; + +// Session options +extern fn hml_ort_create_session_options(err: ptr): ptr; +extern fn hml_ort_release_session_options(opts: ptr): void; +extern fn hml_ort_session_options_set_intra_threads(opts: ptr, num: i32, err: ptr): i32; +extern fn hml_ort_session_options_set_inter_threads(opts: ptr, num: i32, err: ptr): i32; +extern fn hml_ort_session_options_set_graph_opt(opts: ptr, level: i32, err: ptr): i32; + +// Session +extern fn hml_ort_create_session(env: ptr, path: ptr, opts: ptr, err: ptr): ptr; +extern fn hml_ort_create_session_from_array(env: ptr, data: ptr, len: i64, opts: ptr, err: ptr): ptr; +extern fn hml_ort_release_session(session: ptr): void; + +// Model inspection +extern fn hml_ort_session_input_count(session: ptr, err: ptr): i64; +extern fn hml_ort_session_output_count(session: ptr, err: ptr): i64; +extern fn hml_ort_session_input_name(session: ptr, idx: i64, err: ptr): ptr; +extern fn hml_ort_session_output_name(session: ptr, idx: i64, err: ptr): ptr; +extern fn hml_ort_free_name(name: ptr): void; +extern fn hml_ort_session_input_shape(session: ptr, idx: i64, shape: ptr, max_dims: i64, err: ptr): i64; +extern fn hml_ort_session_output_shape(session: ptr, idx: i64, shape: ptr, max_dims: i64, err: ptr): i64; +extern fn hml_ort_session_input_type(session: ptr, idx: i64, err: ptr): i32; +extern fn hml_ort_session_output_type(session: ptr, idx: i64, err: ptr): i32; + +// Memory info +extern fn hml_ort_create_cpu_memory_info(err: ptr): ptr; +extern fn hml_ort_release_memory_info(info: ptr): void; + +// Tensor +extern fn hml_ort_create_tensor(data: ptr, data_len: i64, shape: ptr, ndims: i64, + element_type: i32, mem_info: ptr, err: ptr): ptr; +extern fn hml_ort_release_value(value: ptr): void; +extern fn hml_ort_tensor_data(value: ptr, err: ptr): ptr; +extern fn hml_ort_tensor_ndims(value: ptr, err: ptr): i64; +extern fn hml_ort_tensor_shape(value: ptr, shape_out: ptr, max_dims: i64, err: ptr): i32; +extern fn hml_ort_tensor_element_count(value: ptr, err: ptr): i64; +extern fn hml_ort_tensor_element_type(value: ptr, err: ptr): i32; + +// Inference +extern fn hml_ort_run(session: ptr, input_names: ptr, input_values: ptr, + num_inputs: i64, output_names: ptr, num_outputs: i64, + output_values: ptr, err: ptr): i32; + +// ============================================================================ +// Internal Helpers +// ============================================================================ + +// Create and initialize error holder (pointer to char*) +fn _make_err_holder(): ptr { + let err = alloc(SIZEOF_PTR); + ptr_write_ptr(err, ptr_null()); + return err; +} + +// Check error holder and throw if error occurred, then free holder +fn _check_err(err_holder: ptr): null { + let err_ptr = ptr_read_ptr(err_holder); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err_holder); + throw "onnx error: " + msg; + } + free(err_holder); + return null; +} + +// Check error holder, throw if error, and free an additional pointer on error +fn _check_err_with_cleanup(err_holder: ptr, cleanup_ptr: ptr): null { + let err_ptr = ptr_read_ptr(err_holder); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err_holder); + free(cleanup_ptr); + throw "onnx error: " + msg; + } + free(err_holder); + return null; +} + +// Get element size for a given dtype +fn _dtype_size(dtype: i32): i64 { + if (dtype == DTYPE_FLOAT) { return SIZEOF_F32; } + if (dtype == DTYPE_DOUBLE) { return SIZEOF_F64; } + if (dtype == DTYPE_INT32) { return SIZEOF_I32; } + if (dtype == DTYPE_INT64) { return SIZEOF_I64; } + if (dtype == DTYPE_UINT32) { return SIZEOF_I32; } + if (dtype == DTYPE_UINT64) { return SIZEOF_I64; } + if (dtype == DTYPE_INT16 || dtype == DTYPE_UINT16 || dtype == DTYPE_FLOAT16) { return 2; } + if (dtype == DTYPE_INT8 || dtype == DTYPE_UINT8 || dtype == DTYPE_BOOL) { return 1; } + return 0; +} + +// Compute total element count from shape array +fn _shape_element_count(shape: array): i64 { + let count: i64 = 1; + let i = 0; + while (i < shape.length) { + count = count * shape[i]; + i = i + 1; + } + return count; +} + +// Pack a shape array into contiguous i64 memory +fn _pack_shape(shape: array): ptr { + let ndims = shape.length; + let buf = alloc(ndims * SIZEOF_I64); + let i = 0; + while (i < ndims) { + let dim: i64 = shape[i]; + ptr_write_i64(buf + i * SIZEOF_I64, dim); + i = i + 1; + } + return buf; +} + +// Pack a Hemlock array into contiguous f32 memory +fn _pack_f32(arr: array, count: i64): ptr { + let buf = alloc(count * SIZEOF_F32); + let i = 0; + while (i < count) { + let val: f32 = arr[i]; + ptr_write_f32(buf + i * SIZEOF_F32, val); + i = i + 1; + } + return buf; +} + +// Pack a Hemlock array into contiguous f64 memory +fn _pack_f64(arr: array, count: i64): ptr { + let buf = alloc(count * SIZEOF_F64); + let i = 0; + while (i < count) { + let val: f64 = arr[i]; + ptr_write_f64(buf + i * SIZEOF_F64, val); + i = i + 1; + } + return buf; +} + +// Pack a Hemlock array into contiguous i32 memory +fn _pack_i32(arr: array, count: i64): ptr { + let buf = alloc(count * SIZEOF_I32); + let i = 0; + while (i < count) { + let val: i32 = arr[i]; + ptr_write_i32(buf + i * SIZEOF_I32, val); + i = i + 1; + } + return buf; +} + +// Pack a Hemlock array into contiguous i64 memory +fn _pack_i64(arr: array, count: i64): ptr { + let buf = alloc(count * SIZEOF_I64); + let i = 0; + while (i < count) { + let val: i64 = arr[i]; + ptr_write_i64(buf + i * SIZEOF_I64, val); + i = i + 1; + } + return buf; +} + +// ============================================================================ +// Types +// ============================================================================ + +// ONNX Runtime session wrapping environment, session, and options handles +define Session { + _env: ptr, + _session: ptr, + _options: ptr, + _mem_info: ptr, + _freed: bool, +} + +// Tensor wrapping an OrtValue with its backing data buffer +define Tensor { + _value: ptr, + _data_buf: ptr, + _dtype: i32, + _freed: bool, +} + +// Ensure session is not freed +fn _check_session(s: Session): null { + if (s._freed) { + throw "Session has been freed"; + } + return null; +} + +// Ensure tensor is not freed +fn _check_tensor(t: Tensor): null { + if (t._freed) { + throw "Tensor has been freed"; + } + return null; +} + +// ============================================================================ +// Version +// ============================================================================ + +// Get ONNX Runtime library version string (e.g., "1.17.0") +export fn version(): string { + let ver_ptr = hml_ort_version(); + return __cstr_to_string(ver_ptr); +} + +// Get the ORT API version this wrapper was built against +export fn api_version(): i32 { + return hml_ort_api_version(); +} + +// ============================================================================ +// Session Lifecycle +// ============================================================================ + +// Create an inference session from a model file +// +// model_path: Path to the .onnx model file +// log_level: Logging verbosity (default: LOG_WARNING) +// intra_threads: Number of intra-op threads (default: 0 = auto) +// inter_threads: Number of inter-op threads (default: 0 = auto) +// optimization: Graph optimization level (default: OPT_ALL) +export fn create_session(model_path: string, log_level?: 2, + intra_threads?: 0, inter_threads?: 0, optimization?: 99): Session { + + // Create environment + let logid_cstr = __string_to_cstr("hemlock"); + let err = _make_err_holder(); + let env = hml_ort_create_env(log_level, logid_cstr, err); + free(logid_cstr); + _check_err(err); + + if (env == ptr_null()) { + throw "onnx error: failed to create environment"; + } + + // Create session options + let err2 = _make_err_holder(); + let opts = hml_ort_create_session_options(err2); + + let err_ptr2 = ptr_read_ptr(err2); + if (err_ptr2 != ptr_null()) { + let msg = __cstr_to_string(err_ptr2); + free(err2); + hml_ort_release_env(env); + throw "onnx error: " + msg; + } + free(err2); + + // Configure options + if (intra_threads > 0) { + let err3 = _make_err_holder(); + hml_ort_session_options_set_intra_threads(opts, intra_threads, err3); + let err_ptr3 = ptr_read_ptr(err3); + if (err_ptr3 != ptr_null()) { + let msg = __cstr_to_string(err_ptr3); + free(err3); + hml_ort_release_session_options(opts); + hml_ort_release_env(env); + throw "onnx error: " + msg; + } + free(err3); + } + + if (inter_threads > 0) { + let err4 = _make_err_holder(); + hml_ort_session_options_set_inter_threads(opts, inter_threads, err4); + let err_ptr4 = ptr_read_ptr(err4); + if (err_ptr4 != ptr_null()) { + let msg = __cstr_to_string(err_ptr4); + free(err4); + hml_ort_release_session_options(opts); + hml_ort_release_env(env); + throw "onnx error: " + msg; + } + free(err4); + } + + let err5 = _make_err_holder(); + hml_ort_session_options_set_graph_opt(opts, optimization, err5); + let err_ptr5 = ptr_read_ptr(err5); + if (err_ptr5 != ptr_null()) { + let msg = __cstr_to_string(err_ptr5); + free(err5); + hml_ort_release_session_options(opts); + hml_ort_release_env(env); + throw "onnx error: " + msg; + } + free(err5); + + // Create session + let path_cstr = __string_to_cstr(model_path); + let err6 = _make_err_holder(); + let session = hml_ort_create_session(env, path_cstr, opts, err6); + free(path_cstr); + + let err_ptr6 = ptr_read_ptr(err6); + if (err_ptr6 != ptr_null()) { + let msg = __cstr_to_string(err_ptr6); + free(err6); + hml_ort_release_session_options(opts); + hml_ort_release_env(env); + throw "onnx error: " + msg; + } + free(err6); + + if (session == ptr_null()) { + hml_ort_release_session_options(opts); + hml_ort_release_env(env); + throw "onnx error: failed to create session"; + } + + // Create CPU memory info for tensor operations + let err7 = _make_err_holder(); + let mem_info = hml_ort_create_cpu_memory_info(err7); + let err_ptr7 = ptr_read_ptr(err7); + if (err_ptr7 != ptr_null()) { + let msg = __cstr_to_string(err_ptr7); + free(err7); + hml_ort_release_session(session); + hml_ort_release_session_options(opts); + hml_ort_release_env(env); + throw "onnx error: " + msg; + } + free(err7); + + let s: Session = { + _env: env, + _session: session, + _options: opts, + _mem_info: mem_info, + _freed: false, + }; + return s; +} + +// Free all resources associated with a session +export fn free_session(s: Session): null { + if (s._freed) { + return null; + } + hml_ort_release_memory_info(s._mem_info); + hml_ort_release_session(s._session); + hml_ort_release_session_options(s._options); + hml_ort_release_env(s._env); + s._freed = true; + return null; +} + +// ============================================================================ +// Model Inspection +// ============================================================================ + +// Get number of model inputs +export fn input_count(s: Session): i64 { + _check_session(s); + let err = _make_err_holder(); + let count = hml_ort_session_input_count(s._session, err); + _check_err(err); + return count; +} + +// Get number of model outputs +export fn output_count(s: Session): i64 { + _check_session(s); + let err = _make_err_holder(); + let count = hml_ort_session_output_count(s._session, err); + _check_err(err); + return count; +} + +// Get all input names as an array of strings +export fn input_names(s: Session): array { + _check_session(s); + let count = input_count(s); + let names = []; + let i: i64 = 0; + while (i < count) { + let err = _make_err_holder(); + let name_ptr = hml_ort_session_input_name(s._session, i, err); + _check_err(err); + let name = __cstr_to_string(name_ptr); + hml_ort_free_name(name_ptr); + names.push(name); + i = i + 1; + } + return names; +} + +// Get all output names as an array of strings +export fn output_names(s: Session): array { + _check_session(s); + let count = output_count(s); + let names = []; + let i: i64 = 0; + while (i < count) { + let err = _make_err_holder(); + let name_ptr = hml_ort_session_output_name(s._session, i, err); + _check_err(err); + let name = __cstr_to_string(name_ptr); + hml_ort_free_name(name_ptr); + names.push(name); + i = i + 1; + } + return names; +} + +// Get the shape of an input tensor by index +// Returns array of i64 dimensions (dynamic dimensions may be -1) +export fn input_shape(s: Session, idx: i64): array { + _check_session(s); + let max_dims: i64 = 16; + let shape_buf = alloc(max_dims * SIZEOF_I64); + let err = _make_err_holder(); + let ndims = hml_ort_session_input_shape(s._session, idx, shape_buf, max_dims, err); + + let err_ptr = ptr_read_ptr(err); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err); + free(shape_buf); + throw "onnx error: " + msg; + } + free(err); + + let shape = []; + let i: i64 = 0; + while (i < ndims) { + shape.push(ptr_read_i64(shape_buf + i * SIZEOF_I64)); + i = i + 1; + } + free(shape_buf); + return shape; +} + +// Get the shape of an output tensor by index +export fn output_shape(s: Session, idx: i64): array { + _check_session(s); + let max_dims: i64 = 16; + let shape_buf = alloc(max_dims * SIZEOF_I64); + let err = _make_err_holder(); + let ndims = hml_ort_session_output_shape(s._session, idx, shape_buf, max_dims, err); + + let err_ptr = ptr_read_ptr(err); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err); + free(shape_buf); + throw "onnx error: " + msg; + } + free(err); + + let shape = []; + let i: i64 = 0; + while (i < ndims) { + shape.push(ptr_read_i64(shape_buf + i * SIZEOF_I64)); + i = i + 1; + } + free(shape_buf); + return shape; +} + +// Get the element type of an input tensor (returns DTYPE_* constant) +export fn input_type(s: Session, idx: i64): i32 { + _check_session(s); + let err = _make_err_holder(); + let dtype = hml_ort_session_input_type(s._session, idx, err); + _check_err(err); + return dtype; +} + +// Get the element type of an output tensor (returns DTYPE_* constant) +export fn output_type(s: Session, idx: i64): i32 { + _check_session(s); + let err = _make_err_holder(); + let dtype = hml_ort_session_output_type(s._session, idx, err); + _check_err(err); + return dtype; +} + +// ============================================================================ +// Tensor Creation +// ============================================================================ + +// Create a float32 tensor from data and shape +// +// data: Flat array of f32 values +// shape: Array of dimension sizes (e.g., [1, 3, 224, 224]) +export fn create_tensor_f32(data: array, shape: array): Tensor { + let count = _shape_element_count(shape); + if (data.length != count) { + throw "data length " + data.length + " does not match shape element count " + count; + } + + let data_buf = _pack_f32(data, count); + let shape_buf = _pack_shape(shape); + let data_len = count * SIZEOF_F32; + + let err = _make_err_holder(); + let mem_info = hml_ort_create_cpu_memory_info(err); + _check_err_with_cleanup(err, data_buf); + + let err2 = _make_err_holder(); + let value = hml_ort_create_tensor(data_buf, data_len, shape_buf, shape.length, + DTYPE_FLOAT, mem_info, err2); + + let err_ptr = ptr_read_ptr(err2); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err2); + free(data_buf); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + throw "onnx error: " + msg; + } + free(err2); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + + let t: Tensor = { + _value: value, + _data_buf: data_buf, + _dtype: DTYPE_FLOAT, + _freed: false, + }; + return t; +} + +// Create a float64 (double) tensor from data and shape +export fn create_tensor_f64(data: array, shape: array): Tensor { + let count = _shape_element_count(shape); + if (data.length != count) { + throw "data length " + data.length + " does not match shape element count " + count; + } + + let data_buf = _pack_f64(data, count); + let shape_buf = _pack_shape(shape); + let data_len = count * SIZEOF_F64; + + let err = _make_err_holder(); + let mem_info = hml_ort_create_cpu_memory_info(err); + _check_err_with_cleanup(err, data_buf); + + let err2 = _make_err_holder(); + let value = hml_ort_create_tensor(data_buf, data_len, shape_buf, shape.length, + DTYPE_DOUBLE, mem_info, err2); + + let err_ptr = ptr_read_ptr(err2); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err2); + free(data_buf); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + throw "onnx error: " + msg; + } + free(err2); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + + let t: Tensor = { + _value: value, + _data_buf: data_buf, + _dtype: DTYPE_DOUBLE, + _freed: false, + }; + return t; +} + +// Create an int32 tensor from data and shape +export fn create_tensor_i32(data: array, shape: array): Tensor { + let count = _shape_element_count(shape); + if (data.length != count) { + throw "data length " + data.length + " does not match shape element count " + count; + } + + let data_buf = _pack_i32(data, count); + let shape_buf = _pack_shape(shape); + let data_len = count * SIZEOF_I32; + + let err = _make_err_holder(); + let mem_info = hml_ort_create_cpu_memory_info(err); + _check_err_with_cleanup(err, data_buf); + + let err2 = _make_err_holder(); + let value = hml_ort_create_tensor(data_buf, data_len, shape_buf, shape.length, + DTYPE_INT32, mem_info, err2); + + let err_ptr = ptr_read_ptr(err2); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err2); + free(data_buf); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + throw "onnx error: " + msg; + } + free(err2); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + + let t: Tensor = { + _value: value, + _data_buf: data_buf, + _dtype: DTYPE_INT32, + _freed: false, + }; + return t; +} + +// Create an int64 tensor from data and shape +export fn create_tensor_i64(data: array, shape: array): Tensor { + let count = _shape_element_count(shape); + if (data.length != count) { + throw "data length " + data.length + " does not match shape element count " + count; + } + + let data_buf = _pack_i64(data, count); + let shape_buf = _pack_shape(shape); + let data_len = count * SIZEOF_I64; + + let err = _make_err_holder(); + let mem_info = hml_ort_create_cpu_memory_info(err); + _check_err_with_cleanup(err, data_buf); + + let err2 = _make_err_holder(); + let value = hml_ort_create_tensor(data_buf, data_len, shape_buf, shape.length, + DTYPE_INT64, mem_info, err2); + + let err_ptr = ptr_read_ptr(err2); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err2); + free(data_buf); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + throw "onnx error: " + msg; + } + free(err2); + free(shape_buf); + hml_ort_release_memory_info(mem_info); + + let t: Tensor = { + _value: value, + _data_buf: data_buf, + _dtype: DTYPE_INT64, + _freed: false, + }; + return t; +} + +// Free a tensor's resources +export fn free_tensor(t: Tensor): null { + if (t._freed) { + return null; + } + hml_ort_release_value(t._value); + free(t._data_buf); + t._freed = true; + return null; +} + +// ============================================================================ +// Tensor Inspection +// ============================================================================ + +// Get tensor shape as array of i64 dimensions +export fn tensor_shape(t: Tensor): array { + _check_tensor(t); + let err_ndims = _make_err_holder(); + let ndims = hml_ort_tensor_ndims(t._value, err_ndims); + _check_err(err_ndims); + + if (ndims <= 0) { + return []; + } + + let shape_buf = alloc(ndims * SIZEOF_I64); + let err = _make_err_holder(); + hml_ort_tensor_shape(t._value, shape_buf, ndims, err); + + let err_ptr = ptr_read_ptr(err); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err); + free(shape_buf); + throw "onnx error: " + msg; + } + free(err); + + let shape = []; + let i: i64 = 0; + while (i < ndims) { + shape.push(ptr_read_i64(shape_buf + i * SIZEOF_I64)); + i = i + 1; + } + free(shape_buf); + return shape; +} + +// Get total number of elements in tensor +export fn tensor_element_count(t: Tensor): i64 { + _check_tensor(t); + let err = _make_err_holder(); + let count = hml_ort_tensor_element_count(t._value, err); + _check_err(err); + return count; +} + +// Get tensor element type (returns DTYPE_* constant) +export fn tensor_type(t: Tensor): i32 { + _check_tensor(t); + let err = _make_err_holder(); + let dtype = hml_ort_tensor_element_type(t._value, err); + _check_err(err); + return dtype; +} + +// Read tensor data as array of f32 values +export fn tensor_data_f32(t: Tensor): array { + _check_tensor(t); + let err = _make_err_holder(); + let data_ptr = hml_ort_tensor_data(t._value, err); + _check_err(err); + + let count = tensor_element_count(t); + let result = []; + let i: i64 = 0; + while (i < count) { + result.push(ptr_read_f32(data_ptr + i * SIZEOF_F32)); + i = i + 1; + } + return result; +} + +// Read tensor data as array of f64 values +export fn tensor_data_f64(t: Tensor): array { + _check_tensor(t); + let err = _make_err_holder(); + let data_ptr = hml_ort_tensor_data(t._value, err); + _check_err(err); + + let count = tensor_element_count(t); + let result = []; + let i: i64 = 0; + while (i < count) { + result.push(ptr_read_f64(data_ptr + i * SIZEOF_F64)); + i = i + 1; + } + return result; +} + +// Read tensor data as array of i32 values +export fn tensor_data_i32(t: Tensor): array { + _check_tensor(t); + let err = _make_err_holder(); + let data_ptr = hml_ort_tensor_data(t._value, err); + _check_err(err); + + let count = tensor_element_count(t); + let result = []; + let i: i64 = 0; + while (i < count) { + result.push(ptr_read_i32(data_ptr + i * SIZEOF_I32)); + i = i + 1; + } + return result; +} + +// Read tensor data as array of i64 values +export fn tensor_data_i64(t: Tensor): array { + _check_tensor(t); + let err = _make_err_holder(); + let data_ptr = hml_ort_tensor_data(t._value, err); + _check_err(err); + + let count = tensor_element_count(t); + let result = []; + let i: i64 = 0; + while (i < count) { + result.push(ptr_read_i64(data_ptr + i * SIZEOF_I64)); + i = i + 1; + } + return result; +} + +// ============================================================================ +// Inference +// ============================================================================ + +// Run inference on the model +// +// session: Session object from create_session() +// inputs: Object mapping input names to Tensor values, e.g.: +// { "input_ids": tensor1, "attention_mask": tensor2 } +// OR array of Tensors in model input order +// +// Returns: Array of output Tensor objects +// The caller owns these tensors and must call free_tensor() on each. +export fn run(session: Session, inputs): array { + _check_session(session); + + let in_names = []; + let in_values = []; + + // Determine if inputs is an object (named) or array (positional) + let input_type = typeof(inputs); + + if (input_type == "array") { + // Positional: use model input names in order + let names = input_names(session); + if (inputs.length != names.length) { + throw "expected " + names.length + " inputs, got " + inputs.length; + } + let i = 0; + while (i < inputs.length) { + in_names.push(names[i]); + in_values.push(inputs[i]); + i = i + 1; + } + } else if (input_type == "object") { + // Named: extract keys and values + let names = input_names(session); + let i = 0; + while (i < names.length) { + let name = names[i]; + if (inputs[name] == null) { + throw "missing input tensor for '" + name + "'"; + } + in_names.push(name); + in_values.push(inputs[name]); + i = i + 1; + } + } else { + throw "inputs must be an object or array, got " + input_type; + } + + let num_inputs = in_names.length; + let num_outputs = output_count(session); + let out_names = output_names(session); + + // Build arrays of C string pointers for input names + let in_names_buf = alloc(num_inputs * SIZEOF_PTR); + let in_name_cstrs = []; + let i = 0; + while (i < num_inputs) { + let cstr = __string_to_cstr(in_names[i]); + in_name_cstrs.push(cstr); + ptr_write_ptr(in_names_buf + i * SIZEOF_PTR, cstr); + i = i + 1; + } + + // Build array of OrtValue* pointers for input values + let in_values_buf = alloc(num_inputs * SIZEOF_PTR); + i = 0; + while (i < num_inputs) { + ptr_write_ptr(in_values_buf + i * SIZEOF_PTR, in_values[i]._value); + i = i + 1; + } + + // Build arrays of C string pointers for output names + let out_names_buf = alloc(num_outputs * SIZEOF_PTR); + let out_name_cstrs = []; + i = 0; + while (i < num_outputs) { + let cstr = __string_to_cstr(out_names[i]); + out_name_cstrs.push(cstr); + ptr_write_ptr(out_names_buf + i * SIZEOF_PTR, cstr); + i = i + 1; + } + + // Allocate output values array + let out_values_buf = alloc(num_outputs * SIZEOF_PTR); + memset(out_values_buf, 0, num_outputs * SIZEOF_PTR); + + // Run inference + let err = _make_err_holder(); + let rc = hml_ort_run(session._session, + in_names_buf, in_values_buf, num_inputs, + out_names_buf, num_outputs, out_values_buf, err); + + // Free input name C strings + i = 0; + while (i < num_inputs) { + free(in_name_cstrs[i]); + i = i + 1; + } + free(in_names_buf); + free(in_values_buf); + + // Free output name C strings + i = 0; + while (i < num_outputs) { + free(out_name_cstrs[i]); + i = i + 1; + } + free(out_names_buf); + + // Check for error + let err_ptr = ptr_read_ptr(err); + if (err_ptr != ptr_null()) { + let msg = __cstr_to_string(err_ptr); + free(err); + free(out_values_buf); + throw "onnx error: " + msg; + } + free(err); + + // Wrap output OrtValue* pointers as Tensor objects + let outputs = []; + i = 0; + while (i < num_outputs) { + let out_value = ptr_read_ptr(out_values_buf + i * SIZEOF_PTR); + + // Determine output dtype + let err_dtype = _make_err_holder(); + let dtype = hml_ort_tensor_element_type(out_value, err_dtype); + let dtype_err = ptr_read_ptr(err_dtype); + if (dtype_err != ptr_null()) { + free(err_dtype); + dtype = DTYPE_FLOAT; // fallback + } else { + free(err_dtype); + } + + let t: Tensor = { + _value: out_value, + _data_buf: ptr_null(), // output tensors own their own data + _dtype: dtype, + _freed: false, + }; + outputs.push(t); + i = i + 1; + } + + free(out_values_buf); + return outputs; +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +// Get a human-readable name for a DTYPE_* constant +export fn dtype_name(dtype: i32): string { + if (dtype == DTYPE_FLOAT) { return "float32"; } + if (dtype == DTYPE_DOUBLE) { return "float64"; } + if (dtype == DTYPE_INT8) { return "int8"; } + if (dtype == DTYPE_INT16) { return "int16"; } + if (dtype == DTYPE_INT32) { return "int32"; } + if (dtype == DTYPE_INT64) { return "int64"; } + if (dtype == DTYPE_UINT8) { return "uint8"; } + if (dtype == DTYPE_UINT16) { return "uint16"; } + if (dtype == DTYPE_UINT32) { return "uint32"; } + if (dtype == DTYPE_UINT64) { return "uint64"; } + if (dtype == DTYPE_FLOAT16) { return "float16"; } + if (dtype == DTYPE_BOOL) { return "bool"; } + if (dtype == DTYPE_STRING) { return "string"; } + return "undefined"; +} + +// Describe a session's inputs and outputs (useful for debugging) +export fn describe(session: Session): null { + _check_session(session); + + let in_count = input_count(session); + let out_count = output_count(session); + + print("Model inputs (" + in_count + "):"); + let i: i64 = 0; + while (i < in_count) { + let names = input_names(session); + let shape = input_shape(session, i); + let dtype = input_type(session, i); + print(" [" + i + "] " + names[i] + ": " + dtype_name(dtype) + " " + shape); + i = i + 1; + } + + print("Model outputs (" + out_count + "):"); + i = 0; + while (i < out_count) { + let names = output_names(session); + let shape = output_shape(session, i); + let dtype = output_type(session, i); + print(" [" + i + "] " + names[i] + ": " + dtype_name(dtype) + " " + shape); + i = i + 1; + } + + return null; +} diff --git a/tests/run_tests.sh b/tests/run_tests.sh index aed9d1c8..82500c15 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -144,6 +144,21 @@ for test_file in $TEST_FILES; do fi continue fi + + # Skip ONNX tests if libonnxruntime isn't installed + elif [[ "$category" == "stdlib_onnx" ]]; then + if ! ldconfig -p 2>/dev/null | grep -q libonnxruntime; then + if [ "$category" != "$CURRENT_CATEGORY" ]; then + if [ -n "$CURRENT_CATEGORY" ]; then + echo "" + fi + echo -e "${BLUE}[$category]${NC}" + echo -e "${YELLOW}⊘${NC} Skipping $category tests (libonnxruntime not installed)" + echo " See stdlib/docs/onnx.md for installation instructions" + CURRENT_CATEGORY="$category" + fi + continue + fi fi # Print category header if changed diff --git a/tests/stdlib_onnx/test_basic.hml b/tests/stdlib_onnx/test_basic.hml new file mode 100644 index 00000000..f9ecef43 --- /dev/null +++ b/tests/stdlib_onnx/test_basic.hml @@ -0,0 +1,208 @@ +// Test basic ONNX Runtime module functionality +// +// This test validates the module's API surface without requiring a model file. +// It tests constants, tensor creation, tensor inspection, and error handling. +// +// Requires: libonnxruntime.so and libhemlock_onnx.so + +import { + version, api_version, dtype_name, + create_tensor_f32, create_tensor_f64, create_tensor_i32, create_tensor_i64, + tensor_shape, tensor_element_count, tensor_type, + tensor_data_f32, tensor_data_f64, tensor_data_i32, tensor_data_i64, + free_tensor, + DTYPE_FLOAT, DTYPE_DOUBLE, DTYPE_INT32, DTYPE_INT64, + DTYPE_UINT8, DTYPE_BOOL, DTYPE_STRING, + LOG_VERBOSE, LOG_INFO, LOG_WARNING, LOG_ERROR, LOG_FATAL, + OPT_DISABLE, OPT_BASIC, OPT_EXTENDED, OPT_ALL +} from "@stdlib/onnx"; + +// ============================================================================ +// Test constants +// ============================================================================ + +assert(DTYPE_FLOAT == 1, "DTYPE_FLOAT should be 1"); +assert(DTYPE_DOUBLE == 11, "DTYPE_DOUBLE should be 11"); +assert(DTYPE_INT32 == 6, "DTYPE_INT32 should be 6"); +assert(DTYPE_INT64 == 7, "DTYPE_INT64 should be 7"); + +assert(LOG_VERBOSE == 0, "LOG_VERBOSE should be 0"); +assert(LOG_INFO == 1, "LOG_INFO should be 1"); +assert(LOG_WARNING == 2, "LOG_WARNING should be 2"); +assert(LOG_ERROR == 3, "LOG_ERROR should be 3"); +assert(LOG_FATAL == 4, "LOG_FATAL should be 4"); + +assert(OPT_DISABLE == 0, "OPT_DISABLE should be 0"); +assert(OPT_BASIC == 1, "OPT_BASIC should be 1"); +assert(OPT_EXTENDED == 2, "OPT_EXTENDED should be 2"); +assert(OPT_ALL == 99, "OPT_ALL should be 99"); + +print("Constants OK"); + +// ============================================================================ +// Test dtype_name +// ============================================================================ + +assert(dtype_name(DTYPE_FLOAT) == "float32", "dtype_name(DTYPE_FLOAT)"); +assert(dtype_name(DTYPE_DOUBLE) == "float64", "dtype_name(DTYPE_DOUBLE)"); +assert(dtype_name(DTYPE_INT32) == "int32", "dtype_name(DTYPE_INT32)"); +assert(dtype_name(DTYPE_INT64) == "int64", "dtype_name(DTYPE_INT64)"); +assert(dtype_name(DTYPE_UINT8) == "uint8", "dtype_name(DTYPE_UINT8)"); +assert(dtype_name(DTYPE_BOOL) == "bool", "dtype_name(DTYPE_BOOL)"); +assert(dtype_name(DTYPE_STRING) == "string", "dtype_name(DTYPE_STRING)"); +assert(dtype_name(0) == "undefined", "dtype_name(0)"); + +print("dtype_name OK"); + +// ============================================================================ +// Test version +// ============================================================================ + +let ver = version(); +assert(typeof(ver) == "string", "version() should return string"); +assert(ver.length > 0, "version() should not be empty"); +print("ONNX Runtime version: " + ver); + +let api_ver = api_version(); +assert(typeof(api_ver) == "i32", "api_version() should return i32"); +assert(api_ver > 0, "api_version() should be positive"); +print("API version: " + api_ver); + +// ============================================================================ +// Test tensor creation and inspection - f32 +// ============================================================================ + +let t1 = create_tensor_f32([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]); +assert(t1 != null, "create_tensor_f32 should return a tensor"); + +let shape1 = tensor_shape(t1); +assert(shape1.length == 2, "shape should have 2 dimensions"); +assert(shape1[0] == 2, "first dimension should be 2"); +assert(shape1[1] == 3, "second dimension should be 3"); + +let count1 = tensor_element_count(t1); +assert(count1 == 6, "element count should be 6"); + +let type1 = tensor_type(t1); +assert(type1 == DTYPE_FLOAT, "element type should be DTYPE_FLOAT"); + +let data1 = tensor_data_f32(t1); +assert(data1.length == 6, "data should have 6 elements"); +// Check approximate values (f32 precision) +assert(data1[0] > 0.99 && data1[0] < 1.01, "data[0] should be ~1.0"); +assert(data1[5] > 5.99 && data1[5] < 6.01, "data[5] should be ~6.0"); + +free_tensor(t1); + +print("Tensor f32 OK"); + +// ============================================================================ +// Test tensor creation - f64 +// ============================================================================ + +let t2 = create_tensor_f64([1.5, 2.5, 3.5], [1, 3]); +let shape2 = tensor_shape(t2); +assert(shape2[0] == 1, "f64 first dim should be 1"); +assert(shape2[1] == 3, "f64 second dim should be 3"); +assert(tensor_type(t2) == DTYPE_DOUBLE, "f64 type should be DTYPE_DOUBLE"); + +let data2 = tensor_data_f64(t2); +assert(data2.length == 3, "f64 data should have 3 elements"); +assert(data2[0] > 1.49 && data2[0] < 1.51, "f64 data[0] should be ~1.5"); + +free_tensor(t2); + +print("Tensor f64 OK"); + +// ============================================================================ +// Test tensor creation - i32 +// ============================================================================ + +let t3 = create_tensor_i32([10, 20, 30, 40], [4]); +let shape3 = tensor_shape(t3); +assert(shape3.length == 1, "i32 shape should be 1D"); +assert(shape3[0] == 4, "i32 first dim should be 4"); +assert(tensor_type(t3) == DTYPE_INT32, "i32 type should be DTYPE_INT32"); + +let data3 = tensor_data_i32(t3); +assert(data3[0] == 10, "i32 data[0] should be 10"); +assert(data3[3] == 40, "i32 data[3] should be 40"); + +free_tensor(t3); + +print("Tensor i32 OK"); + +// ============================================================================ +// Test tensor creation - i64 +// ============================================================================ + +let t4 = create_tensor_i64([100, 200], [2, 1]); +let shape4 = tensor_shape(t4); +assert(shape4.length == 2, "i64 shape should be 2D"); +assert(shape4[0] == 2, "i64 first dim should be 2"); +assert(shape4[1] == 1, "i64 second dim should be 1"); +assert(tensor_type(t4) == DTYPE_INT64, "i64 type should be DTYPE_INT64"); + +let data4 = tensor_data_i64(t4); +assert(data4[0] == 100, "i64 data[0] should be 100"); +assert(data4[1] == 200, "i64 data[1] should be 200"); + +free_tensor(t4); + +print("Tensor i64 OK"); + +// ============================================================================ +// Test higher-dimensional tensor +// ============================================================================ + +let t5 = create_tensor_f32( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, + 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0], + [2, 3, 4] +); +let shape5 = tensor_shape(t5); +assert(shape5.length == 3, "3D shape should have 3 dimensions"); +assert(shape5[0] == 2, "3D first dim should be 2"); +assert(shape5[1] == 3, "3D second dim should be 3"); +assert(shape5[2] == 4, "3D third dim should be 4"); +assert(tensor_element_count(t5) == 24, "3D element count should be 24"); + +free_tensor(t5); + +print("Tensor 3D OK"); + +// ============================================================================ +// Test error cases +// ============================================================================ + +// Data/shape mismatch +let shape_error = false; +try { + let bad = create_tensor_f32([1.0, 2.0, 3.0], [2, 2]); +} catch (e) { + shape_error = true; +} +assert(shape_error, "should throw on data/shape mismatch"); + +// Double free (should be safe) +let t6 = create_tensor_f32([1.0], [1]); +free_tensor(t6); +free_tensor(t6); // second free should be no-op + +// Invalid model path +let session_error = false; +try { + let bad_session = create_session("/nonexistent/model.onnx"); +} catch (e) { + session_error = true; +} +assert(session_error, "should throw on invalid model path"); + +print("Error handling OK"); + +// ============================================================================ +// Done +// ============================================================================ + +print("All basic ONNX tests passed!"); diff --git a/tests/stdlib_onnx/test_inference.hml b/tests/stdlib_onnx/test_inference.hml new file mode 100644 index 00000000..5326f022 --- /dev/null +++ b/tests/stdlib_onnx/test_inference.hml @@ -0,0 +1,107 @@ +// Test ONNX Runtime inference with a real model +// +// This test creates a simple ONNX model programmatically using the +// model's protobuf format, then runs inference through it. +// +// Requires: libonnxruntime.so, libhemlock_onnx.so, and a test model file +// +// To generate a test model: +// python3 -c " +// import numpy as np +// import onnx +// from onnx import helper, TensorProto +// +// # Simple model: output = input * 2.0 +// X = helper.make_tensor_value_info('input', TensorProto.FLOAT, [1, 4]) +// Y = helper.make_tensor_value_info('output', TensorProto.FLOAT, [1, 4]) +// weight = helper.make_tensor('weight', TensorProto.FLOAT, [1], [2.0]) +// node = helper.make_node('Mul', ['input', 'weight'], ['output']) +// graph = helper.make_graph([node], 'test', [X], [Y], [weight]) +// model = helper.make_model(graph) +// model.opset_import[0].version = 13 +// onnx.save(model, 'tests/stdlib_onnx/test_mul2.onnx') +// " +// +// If no model file is available, this test prints a skip message. + +import { exists } from "@stdlib/fs"; +import { + create_session, free_session, run, + create_tensor_f32, tensor_data_f32, tensor_shape, free_tensor, + input_count, output_count, input_names, output_names, + input_shape, output_shape, input_type, output_type, + describe, dtype_name, DTYPE_FLOAT +} from "@stdlib/onnx"; + +let model_path = "tests/stdlib_onnx/test_mul2.onnx"; + +if (!exists(model_path)) { + print("Skipping inference test: model file not found (" + model_path + ")"); + print("All inference ONNX tests passed!"); +} else { + // Load model + let session = create_session(model_path); + assert(session != null, "session should not be null"); + + // Inspect model + assert(input_count(session) == 1, "model should have 1 input"); + assert(output_count(session) == 1, "model should have 1 output"); + + let in_names = input_names(session); + assert(in_names[0] == "input", "input name should be 'input'"); + + let out_names = output_names(session); + assert(out_names[0] == "output", "output name should be 'output'"); + + let in_shape = input_shape(session, 0); + assert(in_shape.length == 2, "input should be 2D"); + assert(in_shape[0] == 1, "input batch size should be 1"); + assert(in_shape[1] == 4, "input features should be 4"); + + assert(input_type(session, 0) == DTYPE_FLOAT, "input type should be float32"); + assert(output_type(session, 0) == DTYPE_FLOAT, "output type should be float32"); + + print("Model inspection OK"); + + // Describe model (prints to stdout) + describe(session); + + // Create input tensor + let input = create_tensor_f32([1.0, 2.0, 3.0, 4.0], [1, 4]); + + // Run inference using named inputs + let outputs = run(session, { "input": input }); + assert(outputs.length == 1, "should have 1 output"); + + // Check output + let out_shape = tensor_shape(outputs[0]); + assert(out_shape.length == 2, "output should be 2D"); + assert(out_shape[0] == 1, "output batch should be 1"); + assert(out_shape[1] == 4, "output features should be 4"); + + let result = tensor_data_f32(outputs[0]); + assert(result.length == 4, "result should have 4 elements"); + + // model multiplies by 2.0 + assert(result[0] > 1.99 && result[0] < 2.01, "result[0] should be ~2.0"); + assert(result[1] > 3.99 && result[1] < 4.01, "result[1] should be ~4.0"); + assert(result[2] > 5.99 && result[2] < 6.01, "result[2] should be ~6.0"); + assert(result[3] > 7.99 && result[3] < 8.01, "result[3] should be ~8.0"); + + print("Named inference OK"); + + // Run inference using positional inputs (array) + let outputs2 = run(session, [input]); + let result2 = tensor_data_f32(outputs2[0]); + assert(result2[0] > 1.99 && result2[0] < 2.01, "positional result[0] should be ~2.0"); + + print("Positional inference OK"); + + // Cleanup + free_tensor(outputs[0]); + free_tensor(outputs2[0]); + free_tensor(input); + free_session(session); + + print("All inference ONNX tests passed!"); +}