mcap/modules/onnxruntime/session.jai
Musa Mahmood dd7cf83ca6 [0.1.25] OCR on the GPU runs as many detector and recognizer runs at once as fit in its memory
The workers ran gpu_workers (4) runs at once whatever the images: the server
model, four large images at a time, took 15 GB of a 16 GB GPU and spilled into
shared system memory, and a smaller GPU would fare far worse.

modules/OCR/budget.jai: each run reserves what it is expected to need, and
waits while the runs in flight or the GPU's free memory leave too little; one
run always goes. The need is measured when the engine is made, from the arena's
peak (AllocatorGetStats MaxInUse) over runs on two input sizes, so it holds for
any model and GPU: the mobile detector 6 MB + 137 MB a megapixel, the server's
4 MB + 1015, against 511 and 522 MB measured for a 2560 x 1440 tile. The limit
is the GPU's free memory less 10% (at least 512 MB), read at the start of each
ocr_process call (cudaMemGetInfo, ort_cuda_memory); the free memory is also read
as each run starts, since finished runs' blocks stay in the arena until the
stage ends.

gpu-bench, RTX 5080 laptop: the server model 335 ms a capture (854 before),
peak 13.1 GB (15.4), passes within 4%; the mobile model unchanged (140 ms,
5.7 GB), as its runs fit.
2026-10-17 21:31:17 -04:00

248 lines
12 KiB
Plaintext

// Sessions: one model file each, run on an NVIDIA GPU through CUDA or on the CPU, and running them on
// a tensor. Shared by the modules that run models (OCR, Labels, Segment).
// CUDA sessions are made, first run and released one at a time, process-wide: hold ort_gpu_lock
// around it. Done on two threads at once (onnxruntime 1.30.0, CUDA 13, cuBLASLt 13.8, RTX 5080
// laptop), it crashed with a DEP access violation in cuBLASLt or near it, every time in a debug build;
// one at a time, never. Running sessions on many threads at once is fine. A spin lock, needing no
// setup, as engines can be made before anything else runs; it is held for a second or so at a time.
ort_gpu_lock :: () {
while atomic_swap(*ort_gpu_held, 1) != 0 sleep_milliseconds(2);
}
ort_gpu_unlock :: () {
atomic_swap(*ort_gpu_held, 0);
}
#scope_file
ort_gpu_held: s32;
#scope_export
Ort_Session :: struct {
session: *OrtSession;
shrink_options: *OrtRunOptions; // for a run that shrinks the arena afterwards (ort_run's shrink); null when the settings did not ask for it
input_name, output_name: *u8; // heap C strings
input_shape: [4] s64; // as the model declares it: -1 for a size it leaves open
input_rank: s64;
}
// How a CUDA session uses GPU memory. Its arena takes blocks from CUDA as runs need them, and by
// default never joins or frees them: a model whose input size changes (OCR's) leaves a block of
// every larger size behind, gigabytes of them, unless the arena is shrunk now and then. Shrinking
// after every run costs a third to a half of OCR's speed (CUDA frees and allocates again, waiting for
// the GPU), so OCR shrinks once per call (ocr_process).
Ort_Gpu_Settings :: struct {
arena_extend_strategy := "kNextPowerOfTwo"; // how much the arena takes when it runs short: twice as much each time; "kSameAsRequested" takes what is asked
cudnn_max_workspace := false; // let cuDNN take as much working memory as it finds best (onnxruntime's own default)
shrink_arena := false; // a run with ort_run's shrink set then gives back the arena's blocks no run is using
}
// A session for the model file at path; with CUDA when gpu is set. output names the output that
// ort_run returns; "" for the first. missing_hint is added when the file is not there, to say how
// to get it. On failure, the reason.
ort_create_session :: (env: *OrtEnv, path: string, gpu: bool, intra_op_threads: s32, settings := Ort_Gpu_Settings.{}, output := "", missing_hint := "") -> Ort_Session, ok: bool, error: string {
result: Ort_Session;
api := ort.api;
options: *OrtSessionOptions;
ok, error := ort_check(api.CreateSessionOptions(*options));
if !ok return result, false, error;
defer api.ReleaseSessionOptions(options);
ort_check(api.SetSessionGraphOptimizationLevel(options, .ORT_ENABLE_ALL));
ort_check(api.SetIntraOpNumThreads(options, intra_op_threads));
ort_check(api.SetInterOpNumThreads(options, 1));
if gpu {
cuda: *OrtCUDAProviderOptionsV2;
ok, error = ort_check(api.CreateCUDAProviderOptions(*cuda));
if !ok return result, false, error;
defer api.ReleaseCUDAProviderOptions(cuda);
// Inputs come in many sizes: an exhaustive cuDNN search would be redone for each new shape.
keys := (*u8).["device_id", "cudnn_conv_algo_search", "arena_extend_strategy", "cudnn_conv_use_max_workspace"];
values := (*u8).["0", "HEURISTIC", temp_c_string(settings.arena_extend_strategy), temp_c_string(ifx settings.cudnn_max_workspace then "1" else "0")];
ok, error = ort_check(api.UpdateCUDAProviderOptions(cuda, keys.data, values.data, xx keys.count));
if !ok return result, false, error;
ok, error = ort_check(api.SessionOptionsAppendExecutionProvider_CUDA_V2(options, cuda));
if !ok return result, false, error;
}
if !file_exists(path) return result, false, tprint("% is not there%.", path, ifx missing_hint then tprint("; %", missing_hint) else "");
ok, error = ort_check(api.CreateSession(env, ort_path(path), options, *result.session));
if !ok return result, false, error;
if gpu && settings.shrink_arena {
// Device 0's arena, as device_id above; the CPU's is left alone.
ok, error = ort_check(api.CreateRunOptions(*result.shrink_options));
if ok ok, error = ort_check(api.AddRunConfigEntry(result.shrink_options, "memory.enable_memory_arena_shrinkage", "gpu:0"));
if !ok { ort_release_session(*result); return result, false, error; }
}
allocator: *OrtAllocator;
ort_check(api.GetAllocatorWithDefaultOptions(*allocator));
name: *u8;
ort_check(api.SessionGetInputName(result.session, 0, allocator, *name));
result.input_name = to_c_string(to_string(name),, context.default_allocator);
ort_check(api.AllocatorFree(allocator, name));
// The output: the one named, else the first.
output_count: u64;
ort_check(api.SessionGetOutputCount(result.session, *output_count));
names: [..] string;
names.allocator = temp;
for i: 0..cast(s64) output_count - 1 {
ort_check(api.SessionGetOutputName(result.session, xx i, allocator, *name));
array_add(*names, copy_string(to_string(name),, temp));
ort_check(api.AllocatorFree(allocator, name));
}
chosen := ifx names.count then names[0] else "";
if output {
found := array_find(names, output);
if !found { ort_release_session(*result); return result, false, tprint("% has no output %; it has %.", path, output, names); }
chosen = output;
}
result.output_name = to_c_string(chosen,, context.default_allocator);
input_type: *OrtTypeInfo;
if ort_check(api.SessionGetInputTypeInfo(result.session, 0, *input_type)) {
tensor_info: *OrtTensorTypeAndShapeInfo; // belongs to input_type
if ort_check(api.CastTypeInfoToTensorInfo(input_type, *tensor_info)) && tensor_info {
rank: u64;
ort_check(api.GetDimensionsCount(tensor_info, *rank));
result.input_rank = min(cast(s64) rank, result.input_shape.count);
ort_check(api.GetDimensions(tensor_info, result.input_shape.data, xx result.input_rank));
}
api.ReleaseTypeInfo(input_type);
}
return result, true, "";
}
ort_release_session :: (s: *Ort_Session) {
if s.session ort.api.ReleaseSession(s.session);
if s.shrink_options ort.api.ReleaseRunOptions(s.shrink_options);
free(s.input_name,, context.default_allocator);
free(s.output_name,, context.default_allocator);
s.* = .{};
}
// The GPU's free and total memory in bytes, from the CUDA runtime onnxruntime's CUDA provider loads;
// ok false before a CUDA session has been made, or without CUDA.
ort_cuda_memory :: () -> free: s64, total: s64, ok: bool {
Get_Info :: #type (free: *u64, total: *u64) -> s32 #c_call;
get: Get_Info;
#if OS == .WINDOWS {
module := GetModuleHandleA("cudart64_13.dll");
if module get = cast(Get_Info) GetProcAddress(module, "cudaMemGetInfo");
} else #if OS == .LINUX {
handle := dlopen("libcudart.so.13", RTLD_LAZY);
if handle get = cast(Get_Info) dlsym(handle, "cudaMemGetInfo");
}
if !get return 0, 0, false;
free, total: u64;
if get(*free, *total) != 0 return 0, 0, false;
return cast(s64) free, cast(s64) total, true;
}
// A CUDA session's GPU arena: the bytes its blocks hand out now, and the most they ever have (onnxruntime
// keeps the peak since the session was made). ok false when onnxruntime does not say.
ort_gpu_arena_stats :: (s: *Ort_Session) -> in_use: s64, max_in_use: s64, ok: bool {
api := ort.api;
info: *OrtMemoryInfo;
if !ort_check(api.CreateMemoryInfo("Cuda", .OrtArenaAllocator, 0, .OrtMemTypeDefault, *info)) return 0, 0, false;
defer api.ReleaseMemoryInfo(info);
allocator: *OrtAllocator;
if !ort_check(api.CreateAllocator(s.session, info, *allocator)) return 0, 0, false;
defer api.ReleaseAllocator(allocator);
stats: *OrtKeyValuePairs;
if !ort_check(api.AllocatorGetStats(allocator, *stats)) return 0, 0, false;
defer api.ReleaseKeyValuePairs(stats);
keys, values: **u8;
count: u64;
api.GetKeyValuePairs(stats, *keys, *values, *count);
in_use, max_in_use := -1, -1;
for 0..cast(s64) count - 1 {
key := to_string(keys[it]);
if key == "InUse" in_use = string_to_int(to_string(values[it]));
if key == "MaxInUse" max_in_use = string_to_int(to_string(values[it]));
}
if in_use < 0 || max_in_use < 0 return 0, 0, false;
return in_use, max_in_use, true;
}
// A run's output: its values stay valid until ort_release_output.
Ort_Output :: struct {
value: *OrtValue;
data: *float; // the values, for a float output (every model here has one)
dims: [4] s64;
dim_count: s64;
}
// Runs the session on one input tensor of shape, bytes long at data, with element type element.
// A session may be run from several threads at once. With shrink, and a session made with
// shrink_arena, the arena gives back the blocks no run is using once this run is done.
ort_run :: (s: *Ort_Session, memory_info: *OrtMemoryInfo, data: *void, bytes: s64, element: ONNXTensorElementDataType, shape: [] s64, shrink := false) -> Ort_Output, ok: bool, error: string {
out: Ort_Output;
api := ort.api;
input: *OrtValue;
ok, error := ort_check(api.CreateTensorWithDataAsOrtValue(memory_info, data, xx bytes, shape.data, xx shape.count, element, *input));
if !ok return out, false, error;
defer api.ReleaseValue(input);
ok, error = ort_check(api.Run(s.session, ifx shrink then s.shrink_options else null, *s.input_name, *input, 1, *s.output_name, 1, *out.value));
if !ok return out, false, error;
raw: *void;
ort_check(api.GetTensorMutableData(out.value, *raw));
out.data = cast(*float) raw;
info: *OrtTensorTypeAndShapeInfo;
ort_check(api.GetTensorTypeAndShape(out.value, *info));
count: u64;
ort_check(api.GetDimensionsCount(info, *count));
out.dim_count = min(cast(s64) count, out.dims.count);
ort_check(api.GetDimensions(info, out.dims.data, xx out.dim_count));
api.ReleaseTensorTypeAndShapeInfo(info);
return out, true, "";
}
ort_run_float :: (s: *Ort_Session, memory_info: *OrtMemoryInfo, data: [] float, shape: [] s64, shrink := false) -> Ort_Output, ok: bool, error: string {
out, ok, error := ort_run(s, memory_info, data.data, data.count * size_of(float), .ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, shape, shrink);
return out, ok, error;
}
// As ort_run_float, for a model with several outputs: those named, into outs (one each, in order),
// each released with ort_release_output. On failure none is set.
ort_run_float_outputs :: (s: *Ort_Session, memory_info: *OrtMemoryInfo, data: [] float, shape: [] s64, names: [] string, outs: [] Ort_Output) -> ok: bool, error: string {
assert(names.count == outs.count);
api := ort.api;
input: *OrtValue;
ok, error := ort_check(api.CreateTensorWithDataAsOrtValue(memory_info, data.data, xx (data.count * size_of(float)), shape.data, xx shape.count, .ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, *input));
if !ok return false, error;
defer api.ReleaseValue(input);
output_names := NewArray(names.count, *u8,, temp);
for names output_names[it_index] = temp_c_string(it);
values := NewArray(names.count, *OrtValue, initialized = true,, temp);
ok, error = ort_check(api.Run(s.session, null, *s.input_name, *input, 1, output_names.data, xx names.count, values.data));
if !ok {
for values if it api.ReleaseValue(it);
return false, error;
}
for * out: outs {
out.* = .{value = values[it_index]};
raw: *void;
ort_check(api.GetTensorMutableData(out.value, *raw));
out.data = cast(*float) raw;
info: *OrtTensorTypeAndShapeInfo;
ort_check(api.GetTensorTypeAndShape(out.value, *info));
count: u64;
ort_check(api.GetDimensionsCount(info, *count));
out.dim_count = min(cast(s64) count, out.dims.count);
ort_check(api.GetDimensions(info, out.dims.data, xx out.dim_count));
api.ReleaseTensorTypeAndShapeInfo(info);
}
return true, "";
}
ort_release_output :: (out: *Ort_Output) {
if out.value ort.api.ReleaseValue(out.value);
out.* = .{};
}