- segmentation-plan.html: the plan for panoptic segmentation of images, per library folder, stored
in .mcap chunks (no file version bump), in six phases.
- tools/export_mask2former.py: exports facebook/mask2former-swin-base-coco-panoptic to ONNX (opset 17,
dynamic batch, 384 x 384 input) and checks it against PyTorch: on 25 images, all 180 segments
identical (IoU 1.000). Writes the class list with names for people ("sky-other-merged" is "sky"),
and PyTorch's own segmentations for segment-probe -check.
- modules/Segment: runs the model with onnxruntime (CUDA, else the CPU) and makes segments of its
outputs as transformers' post_process_panoptic_segmentation does: a segment id map (longest side at
most 1024) and each segment's class, score, area and bounds. Matches PyTorch on 99.8% of pixels,
every segment scored 0.6 or more found. 57 ms an image at batch 4 on an RTX 5080 laptop.
- tools/segment_probe.jai (jai first.jai - segment_probe): segments image files; -out draws them,
-check compares with PyTorch, -dump-input writes the model's input.
- modules/onnxruntime: ort_run_float_outputs, for a model with several outputs.
- first.jai copies the model to bin/segment-models. The model itself is not in git
(modules/Segment/models, 462 MB); tools/export_mask2former.py makes it.
192 lines
8.7 KiB
Plaintext
192 lines
8.7 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;
|
|
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. The defaults came from measuring OCR on the sample captures
|
|
// (RTX 5080 laptop): with nearly every input a new size, onnxruntime's exact-size arena filled GPU
|
|
// memory, and cuDNN's largest workspaces made it worse.
|
|
Ort_Gpu_Settings :: struct {
|
|
arena_extend_strategy := "kNextPowerOfTwo"; // reuses freed blocks for other sizes; "kSameAsRequested" grows by exactly what is asked
|
|
cudnn_max_workspace := false; // let cuDNN take as much working memory as it finds best (onnxruntime's own default)
|
|
}
|
|
|
|
// 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;
|
|
|
|
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);
|
|
free(s.input_name,, context.default_allocator);
|
|
free(s.output_name,, context.default_allocator);
|
|
s.* = .{};
|
|
}
|
|
|
|
// 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.
|
|
ort_run :: (s: *Ort_Session, memory_info: *OrtMemoryInfo, data: *void, bytes: s64, element: ONNXTensorElementDataType, shape: [] s64) -> 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, 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) -> 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);
|
|
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.* = .{};
|
|
}
|