mcap/modules/Core_ML/module.jai

177 lines
8.8 KiB
Plaintext

// Core_ML: a Core ML model (an .mlpackage made by coremltools) run on one float tensor, on macOS.
// The models that run faster here than through onnxruntime on the CPU (tools/export_siglip_coreml.py
// says how much) use it; the rest stay with modules/onnxruntime.
//
// model, ok, error := coreml_load(path, "pixel_values", "pooler_output");
// ok, error = coreml_run(model, input, s64.[1, 3, 384, 384], output); // output: the floats it gives
// coreml_free(model);
//
// Loading compiles the package (Core ML's compileModelAtURL, a fraction of a second) into a temporary
// .mlmodelc, removed by coreml_free. The model runs on the CPU and GPU, never the Neural Engine
// unless asked: with float16 weights it can be badly wrong there (SigLIP 2: cosine 0.72 to the CPU).
// A model runs one call at a time (a mutex), from any thread.
Coreml_Units :: enum s64 { // MLComputeUnits
CPU_ONLY :: 0;
CPU_AND_GPU :: 1;
ALL :: 2;
CPU_AND_ENGINE :: 3;
}
Coreml_Model :: struct {
model: *NSObject; // MLModel, retained
compiled: *NSObject; // the .mlmodelc's NSURL, retained; removed on free
input_name, output_name: string;
mutex: Mutex;
}
coreml_load :: (path: string, input_name: string, output_name: string, units := Coreml_Units.CPU_AND_GPU) -> *Coreml_Model, ok: bool, error: string {
init_objective_c(); // before anything Objective-C, the pool included; once per process does it all
pool := objc_new(NSAutoreleasePool);
defer NSAutoreleasePool.drain(pool);
if !file_exists(path) return null, false, tprint("% is not there.", path);
package := NSURL.fileURLWithPath(NSString.getTempString(path));
error: *NSError;
func_compile :: #type (*void, Selector, *NSURL, **NSError) -> *NSURL #c_call;
compiled := (cast(func_compile) objc_msgSend)(objc_getClass("MLModel"), selector("compileModelAtURL:error:"), package, *error);
if !compiled return null, false, tprint("Core ML could not compile %: %", path, error_text(error));
configuration := objc_init(objc_alloc(objc_getClass("MLModelConfiguration")));
defer release(configuration);
func_units :: #type (*void, Selector, s64) -> void #c_call;
(cast(func_units) objc_msgSend)(configuration, selector("setComputeUnits:"), cast(s64) units);
func_load :: #type (*void, Selector, *NSURL, *NSObject, **NSError) -> *NSObject #c_call;
model := (cast(func_load) objc_msgSend)(objc_getClass("MLModel"), selector("modelWithContentsOfURL:configuration:error:"), compiled, configuration, *error);
if !model {
remove_compiled(compiled);
return null, false, tprint("Core ML could not load %: %", path, error_text(error));
}
result := New(Coreml_Model,, context.default_allocator);
result.model = xx retain(model);
result.compiled = xx retain(compiled);
result.input_name = copy_string(input_name,, context.default_allocator);
result.output_name = copy_string(output_name,, context.default_allocator);
init(*result.mutex);
return result, true, "";
}
coreml_free :: (model: *Coreml_Model) {
if !model return;
pool := objc_new(NSAutoreleasePool);
defer NSAutoreleasePool.drain(pool);
release(model.model);
remove_compiled(cast(*NSURL) model.compiled);
release(model.compiled);
destroy(*model.mutex);
free(model.input_name,, context.default_allocator);
free(model.output_name,, context.default_allocator);
free(model,, context.default_allocator);
}
// Runs the model on input, a float32 tensor of shape, and copies its output (float32 or float16) into
// output as float32. ok is false, with why, when it fails or gives a different number of values.
coreml_run :: (model: *Coreml_Model, input: [] float, shape: [] s64, output: [] float) -> ok: bool, error: string {
lock(*model.mutex);
defer unlock(*model.mutex);
pool := objc_new(NSAutoreleasePool);
defer NSAutoreleasePool.drain(pool);
// The input, over the caller's floats: packed, so each stride is the product of the sizes after it.
numbers :: (values: [] s64) -> *NSObject {
objects := NewArray(values.count, *NSObject,, temp);
func_number :: #type (*void, Selector, s64) -> *NSObject #c_call;
for values objects[it_index] = (cast(func_number) objc_msgSend)(objc_getClass("NSNumber"), selector("numberWithLongLong:"), it);
func_array :: #type (*void, Selector, **NSObject, u64) -> *NSObject #c_call;
return (cast(func_array) objc_msgSend)(objc_getClass("NSArray"), selector("arrayWithObjects:count:"), objects.data, xx objects.count);
}
strides := NewArray(shape.count, s64,, temp);
step: s64 = 1;
for < shape { strides[it_index] = step; step *= it; }
if step != input.count return false, tprint("the input has % values, not % for its shape %", input.count, step, shape);
error: *NSError;
func_array_init :: #type (*void, Selector, *void, *NSObject, s64, *NSObject, *void, **NSError) -> *NSObject #c_call;
array := (cast(func_array_init) objc_msgSend)(objc_alloc(objc_getClass("MLMultiArray")), selector("initWithDataPointer:shape:dataType:strides:deallocator:error:"),
input.data, numbers(shape), MULTIARRAY_FLOAT32, numbers(strides), null, *error);
if !array return false, tprint("Core ML could not take the input: %", error_text(error));
autorelease(array);
func_value :: #type (*void, Selector, *NSObject) -> *NSObject #c_call;
value := (cast(func_value) objc_msgSend)(objc_getClass("MLFeatureValue"), selector("featureValueWithMultiArray:"), array);
key := NSString.getTempString(model.input_name);
func_dictionary :: #type (*void, Selector, *NSObject, *NSObject) -> *NSObject #c_call;
dictionary := (cast(func_dictionary) objc_msgSend)(objc_getClass("NSDictionary"), selector("dictionaryWithObject:forKey:"), value, key);
func_provider :: #type (*void, Selector, *NSObject, **NSError) -> *NSObject #c_call;
features := (cast(func_provider) objc_msgSend)(objc_alloc(objc_getClass("MLDictionaryFeatureProvider")), selector("initWithDictionary:error:"), dictionary, *error);
if !features return false, tprint("Core ML could not take the input: %", error_text(error));
autorelease(features);
func_predict :: #type (*void, Selector, *NSObject, **NSError) -> *NSObject #c_call;
result := (cast(func_predict) objc_msgSend)(model.model, selector("predictionFromFeatures:error:"), features, *error);
if !result return false, tprint("Core ML could not run the model: %", error_text(error));
out_value := (cast(func_value) objc_msgSend)(result, selector("featureValueForName:"), NSString.getTempString(model.output_name));
out_array := ifx out_value then objc_msgSend_typed(out_value, selector("multiArrayValue")) else null;
if !out_array return false, tprint("the model gives no %", model.output_name);
count := cast(s64) objc_msgSend_typed(out_array, selector("count"));
if count != output.count return false, tprint("the model gives % values, not %", count, output.count);
func_type :: #type (*void, Selector) -> s64 #c_call;
type := (cast(func_type) objc_msgSend)(out_array, selector("dataType"));
// The output is packed (one row of the pooled vector here); dataPointer is read before the pool drains.
data := objc_msgSend_typed(out_array, selector("dataPointer"));
if type == MULTIARRAY_FLOAT32 {
memcpy(output.data, data, count * size_of(float));
} else if type == MULTIARRAY_FLOAT16 {
half := cast(*u16) data;
for 0..count - 1 output[it] = float16_to_float32(half[it]);
} else {
return false, tprint("the model's output is of type 0x%, not float32 or float16", formatInt(type, base = 16));
}
return true, "";
}
#scope_file
MULTIARRAY_FLOAT32 :: 0x10000 | 32; // MLMultiArrayDataTypeFloat32
MULTIARRAY_FLOAT16 :: 0x10000 | 16; // MLMultiArrayDataTypeFloat16
error_text :: (error: *NSError) -> string {
if !error return "no reason given";
return copy_string(to_string(NSError.localizedDescription(error)),, temp);
}
remove_compiled :: (url: *NSURL) {
func :: #type (*void, Selector, *NSURL, **NSError) -> BOOL #c_call;
(cast(func) objc_msgSend)(NSFileManager.defaultManager(), selector("removeItemAtURL:error:"), url, null);
}
float16_to_float32 :: (h: u16) -> float {
sign := (cast(u32) h & 0x8000) << 16;
exponent := (cast(u32) h >> 10) & 0x1F;
mantissa := cast(u32) h & 0x3FF;
bits: u32;
if exponent == 0 {
if mantissa == 0 bits = sign;
else { // subnormal: normalised
e: u32 = 113;
while !(mantissa & 0x400) { mantissa <<= 1; e -= 1; }
bits = sign | (e << 23) | ((mantissa & 0x3FF) << 13);
}
} else if exponent == 31 bits = sign | 0x7F80_0000 | (mantissa << 13);
else bits = sign | ((exponent + 112) << 23) | (mantissa << 13);
return (cast(*float) *bits).*;
}
CoreML_Framework :: #library,system,link_always "CoreML";
#import "Basic";
#import "File";
#import "File_Utilities";
#import "Thread";
#import "Objective_C";
#import "Objective_C/AppKit";