mcap/modules/onnxruntime/module.jai
Musa Mahmood 8c8b2c6e38 [0.1.24] GPU memory: OCR runs largest first, gives its arenas' blocks back after each stage, and keeps its worker threads
OCR filled the GPU: 9 GB for the mobile model on gpu-bench's 30 images, past
16 GB for the server model, which Windows then paged into shared memory (1.5 to
11 s a capture). onnxruntime's logged arena extensions showed why: the arena
keeps every block it takes from CUDA and never joins or frees them, and OCR's
inputs grow from run to run (recognizer batches went narrowest first; images in
any order across calls), so each larger run left another block behind: 5.7 GB
of recognizer blocks where the widest batch needs 0.9 GB.

- Detection runs the largest images first, recognition the widest batches.
- The detector's and recognizer's arenas shrink once each stage of an
  ocr_process call is done, by a run on a small input with
  memory.enable_memory_arena_shrinkage. Shrinking after every run cost 33 to
  59% of the speed.
- The worker threads are made with the engine and kept, not made for every
  stage of every call (onnxruntime keeps GPU state for each thread that runs a
  session, microsoft/onnxruntime#33037); 6% faster.
- OCR's arenas grow by what is asked (kSameAsRequested).
- ort.log_level sets what the engines' onnxruntime environments log; INFO shows
  every arena extension.

Mobile OCR on gpu-bench (RTX 5080 laptop, 4 workers): peak 5.7 GB over the
baseline, 2 GB between calls, 142 ms a capture, against 9.0 GB and 126 ms. The
text read differs by a line in 2,986 (batches are grouped differently). The
server model is not yet measured again.
2026-10-17 21:12:57 -04:00

116 lines
4.7 KiB
Plaintext

// onnxruntime, loaded at run time. Nothing is linked: ort_load finds the library, takes its one
// export, OrtGetApiBase, and from it the OrtApi function table (bindings.jai, from generate.jai).
// A program that imports this module starts without onnxruntime and only fails in ort_load.
//
// Version: see fetch.jai (ORT_VERSION) and <os>/VERSION.txt. The bindings are for ORT_API_VERSION;
// an older library is refused, a newer one serves this version of the table.
ort: struct {
api: *OrtApi;
library_path: string; // the library that was loaded
version: string; // onnxruntime's own version string, such as 1.30.0
// What the engines' environments log, to the console: set before the first engine is made, as
// onnxruntime keeps one environment per process. INFO shows each GPU arena extension and its size.
log_level := OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING;
}
// Loads onnxruntime from the first folder that has it: beside the running executable, then each
// of folders. A full path is always used: Windows keeps an older onnxruntime.dll in System32 for
// its own use, which a bare name could find first.
ort_load :: (folders: [] string = .[]) -> ok: bool, error: string {
if ort.api return true, "";
candidates: [..] string;
candidates.allocator = temp;
array_add(*candidates, path_strip_filename(get_path_of_running_executable()));
for folders array_add(*candidates, it);
tried: [..] string;
tried.allocator = temp;
for folder: candidates {
path := tprint("%/%", trim_right(folder, "/\\"), ORT_LIBRARY_NAME);
if !file_exists(path) { array_add(*tried, path); continue; }
get_api_base, load_error := ort_open_library(path);
if !get_api_base return false, load_error;
base := get_api_base();
if !base return false, tprint("% returned no API.", path);
version := to_string(base.GetVersionString());
api := base.GetApi(ORT_API_VERSION);
if !api return false, tprint("% is onnxruntime %, which does not offer API version % (these bindings); use the version fetch.jai pins.", path, version, ORT_API_VERSION);
ort.api = api;
ort.library_path = copy_string(path,, context.default_allocator);
ort.version = copy_string(version,, context.default_allocator);
return true, "";
}
return false, tprint("% was not found. Looked for:\n %\nRun `jai fetch.jai` in modules/onnxruntime and rebuild, so the build copies it beside the executable.",
ORT_LIBRARY_NAME, join(..tried, "\n ",, temp));
}
// A failed status as its message, and the status released. ok and "" for success (a null status).
ort_check :: (status: OrtStatusPtr) -> ok: bool, message: string {
if !status return true, "";
message := tprint("%", to_string(ort.api.GetErrorMessage(status)));
ort.api.ReleaseStatus(status);
return false, message;
}
// A path as the C API takes it: UTF-16 on Windows, UTF-8 elsewhere. The bindings were generated on
// Windows, where ORTCHAR_T is a wchar_t, so elsewhere the pointer is cast to the type they expect. Temp.
ort_path :: (path: string) -> *ORTCHAR_T {
#if OS == .WINDOWS {
return utf8_to_wide(path,, temp);
} else {
return cast(*ORTCHAR_T) temp_c_string(path);
}
}
#load "bindings.jai";
#load "session.jai";
#load "cuda.jai";
#scope_module
Get_Api_Base :: #type () -> *OrtApiBase #c_call;
#if OS == .WINDOWS {
ORT_LIBRARY_NAME :: "onnxruntime.dll";
ort_open_library :: (path: string) -> Get_Api_Base, error: string {
// Altered search path: the libraries onnxruntime loads in turn (onnxruntime_providers_shared.dll,
// onnxruntime_providers_cuda.dll) are looked for in its folder.
windows_path := replace(path, "/", "\\",, temp);
module := LoadLibraryExW(utf8_to_wide(windows_path,, temp), null, LOAD_WITH_ALTERED_SEARCH_PATH);
if !module return null, tprint("Could not load % (Windows error %).", path, GetLastError());
get_api_base := cast(Get_Api_Base) GetProcAddress(module, "OrtGetApiBase");
if !get_api_base return null, tprint("% has no OrtGetApiBase; it is not onnxruntime.", path);
return get_api_base, "";
}
#import "Windows";
#import "Windows_Utf8";
} else {
#if OS == .MACOS ORT_LIBRARY_NAME :: "libonnxruntime.dylib";
else ORT_LIBRARY_NAME :: "libonnxruntime.so";
ort_open_library :: (path: string) -> Get_Api_Base, error: string {
handle := dlopen(temp_c_string(path), RTLD_NOW);
if !handle return null, tprint("Could not load %: %", path, to_string(dlerror()));
get_api_base := cast(Get_Api_Base) dlsym(handle, "OrtGetApiBase");
if !get_api_base return null, tprint("% has no OrtGetApiBase; it is not onnxruntime.", path);
return get_api_base, "";
}
#import "POSIX";
}
#import "Basic";
#import "String";
#import "File";
#import "File_Utilities";
#import "System";
#import "Sort";
#import "Atomics";