Files
ANE/ane_universal.h
zeekay 8d7cd859d3 Fix ANE private API on M1: correct blob header format (128-byte with magic)
The original probes used a 64-byte zero header for weight blobs, which
the M1 ANE compiler silently rejects. The correct format requires a
128-byte header with magic bytes 0xEFBEADDE at offset 64, data size
at offset 72, and data offset (128) at offset 80.

With the correct blob format, _ANEInMemoryModel works on ALL Apple
Silicon (M1+), not just M4. Conv ops compile and run successfully.
The dynamic pipeline's matmul+slice_by_size pattern still fails on
M1-M3 (different hardware limitation).

M1 Max results: train_large runs at 206ms/step, 0.45 TFLOPS ANE.

Changes:
- ane_universal.h: Fix detection probe blob format, update comments
- ane_universal_test.m: Fix test blob format, M1 now uses InMemory path
- ane_m1_multiweight.m: New test validating multi-weight compilation
- .gitignore: Add new binaries, training data files
2026-03-03 06:55:25 -08:00

475 lines
20 KiB
Objective-C

// ane_universal.h — Universal ANE runtime for M1→M4+ Apple Silicon
// Strategy:
// All chips: _ANEInMemoryModel with proper ANE blob format (128-byte header)
// Fallback: CoreML compile → _ANEModel (slower, ~60-280ms)
// Both: same IOSurface I/O for evaluation
//
// Key finding: The private API works on ALL Apple Silicon (M1+) when weight
// blobs use the correct format: 128-byte header with magic 0xEFBEADDE at
// offset 64, data size at 72, data offset (128) at 80. Conv ops with separate
// weight files work across all chips. Matmul/slice_by_size fails on M1-M3.
//
// Usage:
// ane_universal_init()
// UANEKernel *k = ane_universal_compile(milText, weightData, ...)
// ane_universal_write_input(k, 0, data, bytes)
// ane_universal_eval(k)
// ane_universal_read_output(k, 0, data, bytes)
// ane_universal_free(k)
#pragma once
#import <Foundation/Foundation.h>
#import <CoreML/CoreML.h>
#import <objc/runtime.h>
#import <objc/message.h>
#import <dlfcn.h>
#import <IOSurface/IOSurface.h>
typedef enum {
ANE_BACKEND_NONE = 0,
ANE_BACKEND_INMEM, // M4+ private API (_ANEInMemoryModel)
ANE_BACKEND_COREML, // M1-M3 CoreML fallback
} ANEBackendType;
typedef struct {
ANEBackendType backend;
// Shared I/O
IOSurfaceRef *ioInputs;
IOSurfaceRef *ioOutputs;
int nInputs, nOutputs;
size_t *inputBytes;
size_t *outputBytes;
// InMem backend (M4+)
id inMemModel;
id inMemRequest;
NSString *tmpDir;
// CoreML backend (M1-M3)
MLModel *coremlModel;
NSString *coremlInputName;
NSString *coremlOutputName;
int *inputShapes; // [B, C, H, W] per input
int *outputShapes; // [B, C, H, W] per output
NSString *coremlCompiledPath;
} UANEKernel;
static Class g_UANEDesc, g_UANEInMem, g_UANEReq, g_UANEIO;
static ANEBackendType g_ane_backend = ANE_BACKEND_NONE;
static bool g_ane_universal_inited = false;
// ============================================================
// Detect which backend works on this hardware
// ============================================================
static ANEBackendType ane_detect_backend(void) {
dlopen("/System/Library/PrivateFrameworks/AppleNeuralEngine.framework/AppleNeuralEngine", RTLD_NOW);
g_UANEDesc = NSClassFromString(@"_ANEInMemoryModelDescriptor");
g_UANEInMem = NSClassFromString(@"_ANEInMemoryModel");
g_UANEReq = NSClassFromString(@"_ANERequest");
g_UANEIO = NSClassFromString(@"_ANEIOSurfaceObject");
if (!g_UANEDesc || !g_UANEInMem || !g_UANEReq || !g_UANEIO) {
fprintf(stderr, "[ANE] Private classes not found, using CoreML fallback\n");
return ANE_BACKEND_COREML;
}
// Test compile a minimal MIL program to check if private API works
// NOTE: Weight blob must use proper ANE format (128-byte header with magic)
int C = 16, S = 4;
int wsize = C * C * 2;
int total = 128 + wsize;
NSMutableData *wd = [NSMutableData dataWithLength:total];
uint8_t *wb = (uint8_t *)wd.mutableBytes;
wb[0] = 1; wb[4] = 2;
wb[64] = 0xEF; wb[65] = 0xBE; wb[66] = 0xAD; wb[67] = 0xDE; wb[68] = 1;
*(uint32_t *)(wb + 72) = wsize;
*(uint32_t *)(wb + 80) = 128;
uint16_t *wp = (uint16_t *)(wb + 128);
for (int i = 0; i < C * C; i++) wp[i] = 0x3C00;
NSString *testMil = [NSString stringWithFormat:
@"program(1.3)\n"
"[buildInfo = dict<string, string>({{\"coremlc-component-MIL\", \"3510.2.1\"}, "
"{\"coremlc-version\", \"3505.4.1\"}, {\"coremltools-component-milinternal\", \"\"}, "
"{\"coremltools-version\", \"9.0\"}})]\n"
"{\n"
" func main<ios18>(tensor<fp16, [1, %d, 1, %d]> x) {\n"
" tensor<fp16, [%d,%d,1,1]> W = const()[name=string(\"W\"), val=tensor<fp16, [%d,%d,1,1]>(BLOBFILE(path=string(\"@model_path/weights/weight.bin\"), offset=uint64(64)))];\n"
" string pt = const()[name=string(\"pt\"), val=string(\"valid\")];\n"
" tensor<int32, [2]> st = const()[name=string(\"st\"), val=tensor<int32, [2]>([1,1])];\n"
" tensor<int32, [4]> pd = const()[name=string(\"pd\"), val=tensor<int32, [4]>([0,0,0,0])];\n"
" tensor<int32, [2]> dl = const()[name=string(\"dl\"), val=tensor<int32, [2]>([1,1])];\n"
" int32 gr = const()[name=string(\"gr\"), val=int32(1)];\n"
" tensor<fp16, [1,%d,1,%d]> out = conv(dilations=dl,groups=gr,pad=pd,pad_type=pt,strides=st,weight=W,x=x)[name=string(\"out\")];\n"
" } -> (out);\n"
"}\n",
C, S, C, C, C, C, C, S];
NSData *milData = [testMil dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *wdict = @{@"@model_path/weights/weight.bin": @{@"offset": @0, @"data": wd}};
id desc = ((id(*)(Class,SEL,id,id,id))objc_msgSend)(
g_UANEDesc, @selector(modelWithMILText:weights:optionsPlist:),
milData, wdict, nil);
if (!desc) {
fprintf(stderr, "[ANE] Descriptor creation failed, using CoreML fallback\n");
return ANE_BACKEND_COREML;
}
id model = ((id(*)(Class,SEL,id))objc_msgSend)(
g_UANEInMem, @selector(inMemoryModelWithDescriptor:), desc);
if (!model) {
fprintf(stderr, "[ANE] Model creation failed, using CoreML fallback\n");
return ANE_BACKEND_COREML;
}
id hexId = ((id(*)(id,SEL))objc_msgSend)(model, @selector(hexStringIdentifier));
NSString *td = [NSTemporaryDirectory() stringByAppendingPathComponent:hexId];
NSFileManager *fm = [NSFileManager defaultManager];
[fm createDirectoryAtPath:[td stringByAppendingPathComponent:@"weights"]
withIntermediateDirectories:YES attributes:nil error:nil];
[milData writeToFile:[td stringByAppendingPathComponent:@"model.mil"] atomically:YES];
[wd writeToFile:[td stringByAppendingPathComponent:@"weights/weight.bin"] atomically:YES];
NSError *e = nil;
BOOL ok = ((BOOL(*)(id,SEL,unsigned int,id,NSError**))objc_msgSend)(
model, @selector(compileWithQoS:options:error:), 21, @{}, &e);
// Cleanup
((BOOL(*)(id,SEL,unsigned int,NSError**))objc_msgSend)(
model, @selector(unloadWithQoS:error:), 21, &e);
[fm removeItemAtPath:td error:nil];
if (ok) {
fprintf(stderr, "[ANE] In-memory compilation works → using private API\n");
return ANE_BACKEND_INMEM;
} else {
fprintf(stderr, "[ANE] In-memory compilation failed → using CoreML fallback\n");
return ANE_BACKEND_COREML;
}
}
static void ane_universal_init(void) {
if (g_ane_universal_inited) return;
g_ane_backend = ane_detect_backend();
g_ane_universal_inited = true;
}
static IOSurfaceRef ane_u_create_surface(size_t bytes) {
return IOSurfaceCreate((__bridge CFDictionaryRef)@{
(id)kIOSurfaceWidth: @(bytes),
(id)kIOSurfaceHeight: @1,
(id)kIOSurfaceBytesPerElement: @1,
(id)kIOSurfaceBytesPerRow: @(bytes),
(id)kIOSurfaceAllocSize: @(bytes),
(id)kIOSurfacePixelFormat: @0
});
}
// ============================================================
// InMem backend (M4+) — same as original ane_runtime.h
// ============================================================
static UANEKernel *ane_compile_inmem(NSData *milText, NSData *weightData,
int nInputs, size_t *inputSizes,
int nOutputs, size_t *outputSizes) {
NSError *e = nil;
NSDictionary *wdict = weightData ?
@{@"@model_path/weights/weight.bin": @{@"offset": @0, @"data": weightData}} : nil;
id desc = ((id(*)(Class,SEL,id,id,id))objc_msgSend)(
g_UANEDesc, @selector(modelWithMILText:weights:optionsPlist:),
milText, wdict, nil);
if (!desc) return NULL;
id mdl = ((id(*)(Class,SEL,id))objc_msgSend)(
g_UANEInMem, @selector(inMemoryModelWithDescriptor:), desc);
if (!mdl) return NULL;
id hx = ((id(*)(id,SEL))objc_msgSend)(mdl, @selector(hexStringIdentifier));
NSString *td = [NSTemporaryDirectory() stringByAppendingPathComponent:hx];
NSFileManager *fm = [NSFileManager defaultManager];
[fm createDirectoryAtPath:[td stringByAppendingPathComponent:@"weights"]
withIntermediateDirectories:YES attributes:nil error:nil];
[milText writeToFile:[td stringByAppendingPathComponent:@"model.mil"] atomically:YES];
if (weightData)
[weightData writeToFile:[td stringByAppendingPathComponent:@"weights/weight.bin"] atomically:YES];
if (!((BOOL(*)(id,SEL,unsigned int,id,NSError**))objc_msgSend)(
mdl, @selector(compileWithQoS:options:error:), 21, @{}, &e)) {
[fm removeItemAtPath:td error:nil];
return NULL;
}
if (!((BOOL(*)(id,SEL,unsigned int,id,NSError**))objc_msgSend)(
mdl, @selector(loadWithQoS:options:error:), 21, @{}, &e)) {
[fm removeItemAtPath:td error:nil];
return NULL;
}
UANEKernel *k = calloc(1, sizeof(UANEKernel));
k->backend = ANE_BACKEND_INMEM;
k->inMemModel = mdl;
k->tmpDir = td;
k->nInputs = nInputs;
k->nOutputs = nOutputs;
k->inputBytes = malloc(nInputs * sizeof(size_t));
k->outputBytes = malloc(nOutputs * sizeof(size_t));
memcpy(k->inputBytes, inputSizes, nInputs * sizeof(size_t));
memcpy(k->outputBytes, outputSizes, nOutputs * sizeof(size_t));
k->ioInputs = malloc(nInputs * sizeof(IOSurfaceRef));
k->ioOutputs = malloc(nOutputs * sizeof(IOSurfaceRef));
for (int i = 0; i < nInputs; i++)
k->ioInputs[i] = ane_u_create_surface(inputSizes[i]);
for (int i = 0; i < nOutputs; i++)
k->ioOutputs[i] = ane_u_create_surface(outputSizes[i]);
NSMutableArray *wIns = [NSMutableArray arrayWithCapacity:nInputs];
NSMutableArray *iIdx = [NSMutableArray arrayWithCapacity:nInputs];
for (int i = 0; i < nInputs; i++) {
[wIns addObject:((id(*)(Class,SEL,IOSurfaceRef))objc_msgSend)(
g_UANEIO, @selector(objectWithIOSurface:), k->ioInputs[i])];
[iIdx addObject:@(i)];
}
NSMutableArray *wOuts = [NSMutableArray arrayWithCapacity:nOutputs];
NSMutableArray *oIdx = [NSMutableArray arrayWithCapacity:nOutputs];
for (int i = 0; i < nOutputs; i++) {
[wOuts addObject:((id(*)(Class,SEL,IOSurfaceRef))objc_msgSend)(
g_UANEIO, @selector(objectWithIOSurface:), k->ioOutputs[i])];
[oIdx addObject:@(i)];
}
k->inMemRequest = ((id(*)(Class,SEL,id,id,id,id,id,id,id))objc_msgSend)(
g_UANEReq, @selector(requestWithInputs:inputIndices:outputs:outputIndices:weightsBuffer:perfStats:procedureIndex:),
wIns, iIdx, wOuts, oIdx, nil, nil, @0);
return k;
}
// ============================================================
// CoreML backend (M1-M3) — compile mlpackage, load, predict
// ============================================================
// Generate an mlpackage on disk with given weights for CoreML to compile
static NSString *ane_coreml_gen_package(NSData *milText, NSData *weightData) {
static int counter = 0;
NSString *pkgDir = [NSTemporaryDirectory() stringByAppendingPathComponent:
[NSString stringWithFormat:@"ane_coreml_%d_%d", getpid(), counter++]];
NSString *dataDir = [pkgDir stringByAppendingPathComponent:@"Data/com.apple.CoreML"];
NSString *weightsDir = [dataDir stringByAppendingPathComponent:@"weights"];
NSFileManager *fm = [NSFileManager defaultManager];
[fm createDirectoryAtPath:weightsDir withIntermediateDirectories:YES attributes:nil error:nil];
// Write MIL text
[milText writeToFile:[dataDir stringByAppendingPathComponent:@"model.mil"] atomically:YES];
// Write weights
if (weightData)
[weightData writeToFile:[weightsDir stringByAppendingPathComponent:@"weight.bin"] atomically:YES];
// Write Manifest.json
NSString *manifest = @"{\n"
" \"fileFormatVersion\": \"1.0.0\",\n"
" \"itemInfoEntries\": {\n"
" \"WEIGHTS\": {\n"
" \"author\": \"com.apple.CoreML\",\n"
" \"description\": \"CoreML Model Weights\",\n"
" \"name\": \"weights\",\n"
" \"path\": \"com.apple.CoreML/weights\"\n"
" },\n"
" \"MODEL\": {\n"
" \"author\": \"com.apple.CoreML\",\n"
" \"description\": \"CoreML Model Specification\",\n"
" \"name\": \"model.mlmodel\",\n"
" \"path\": \"com.apple.CoreML/model.mlmodel\"\n"
" }\n"
" },\n"
" \"rootModelIdentifier\": \"MODEL\"\n"
"}\n";
[manifest writeToFile:[pkgDir stringByAppendingPathComponent:@"Manifest.json"]
atomically:YES encoding:NSUTF8StringEncoding error:nil];
return pkgDir;
}
static UANEKernel *ane_compile_coreml(NSString *milText, NSData *weightData,
int nInputs, size_t *inputSizes,
int nOutputs, size_t *outputSizes) {
// For CoreML path, we need a proper mlpackage
// The caller provides MIL text in v1.3 format
NSData *milData = [milText dataUsingEncoding:NSUTF8StringEncoding];
NSString *pkgPath = ane_coreml_gen_package(milData, weightData);
NSError *e = nil;
NSURL *compiledURL = [MLModel compileModelAtURL:
[NSURL fileURLWithPath:[pkgPath stringByAppendingPathExtension:@"mlpackage"]]
error:&e];
// The mlpackage approach won't work with raw MIL text since CoreML expects
// protobuf model.mlmodel. Instead, use coremltools from Python.
// For the C-only path, we compile via _ANEModel using the mlmodelc from CoreML.
// Cleanup
[[NSFileManager defaultManager] removeItemAtPath:pkgPath error:nil];
if (!compiledURL) {
fprintf(stderr, "[ANE-CoreML] Compile failed: %s\n", [[e description] UTF8String]);
return NULL;
}
// Load with ANE compute units
MLModelConfiguration *config = [[MLModelConfiguration alloc] init];
config.computeUnits = MLComputeUnitsCPUAndNeuralEngine;
MLModel *model = [MLModel modelWithContentsOfURL:compiledURL
configuration:config error:&e];
if (!model) {
fprintf(stderr, "[ANE-CoreML] Load failed: %s\n", [[e description] UTF8String]);
return NULL;
}
UANEKernel *k = calloc(1, sizeof(UANEKernel));
k->backend = ANE_BACKEND_COREML;
k->coremlModel = model;
k->coremlCompiledPath = [compiledURL path];
k->nInputs = nInputs;
k->nOutputs = nOutputs;
k->inputBytes = malloc(nInputs * sizeof(size_t));
k->outputBytes = malloc(nOutputs * sizeof(size_t));
memcpy(k->inputBytes, inputSizes, nInputs * sizeof(size_t));
memcpy(k->outputBytes, outputSizes, nOutputs * sizeof(size_t));
// Also create IOSurfaces for compatibility
k->ioInputs = malloc(nInputs * sizeof(IOSurfaceRef));
k->ioOutputs = malloc(nOutputs * sizeof(IOSurfaceRef));
for (int i = 0; i < nInputs; i++)
k->ioInputs[i] = ane_u_create_surface(inputSizes[i]);
for (int i = 0; i < nOutputs; i++)
k->ioOutputs[i] = ane_u_create_surface(outputSizes[i]);
// Get input/output names
MLModelDescription *desc = model.modelDescription;
k->coremlInputName = desc.inputDescriptionsByName.allKeys.firstObject;
k->coremlOutputName = desc.outputDescriptionsByName.allKeys.firstObject;
return k;
}
// ============================================================
// Unified API
// ============================================================
static void ane_universal_write_input(UANEKernel *k, int idx, const void *data, size_t bytes) {
IOSurfaceLock(k->ioInputs[idx], 0, NULL);
memcpy(IOSurfaceGetBaseAddress(k->ioInputs[idx]), data, bytes);
IOSurfaceUnlock(k->ioInputs[idx], 0, NULL);
}
static void ane_universal_read_output(UANEKernel *k, int idx, void *data, size_t bytes) {
IOSurfaceLock(k->ioOutputs[idx], kIOSurfaceLockReadOnly, NULL);
memcpy(data, IOSurfaceGetBaseAddress(k->ioOutputs[idx]), bytes);
IOSurfaceUnlock(k->ioOutputs[idx], kIOSurfaceLockReadOnly, NULL);
}
static bool ane_universal_eval(UANEKernel *k) {
if (k->backend == ANE_BACKEND_INMEM) {
NSError *e = nil;
return ((BOOL(*)(id,SEL,unsigned int,id,id,NSError**))objc_msgSend)(
k->inMemModel, @selector(evaluateWithQoS:options:request:error:),
21, @{}, k->inMemRequest, &e);
} else if (k->backend == ANE_BACKEND_COREML) {
// Read from IOSurface into MLMultiArray, predict, write back
NSError *e = nil;
size_t nElems = k->inputBytes[0] / 2; // fp16 = 2 bytes each
// Create float32 input array from fp16 IOSurface data
// Determine shape from bytes (assuming [1, C, 1, S])
MLMultiArray *inputArray = [[MLMultiArray alloc]
initWithShape:@[@1, @(nElems)]
dataType:MLMultiArrayDataTypeFloat32 error:&e];
if (!inputArray) return false;
IOSurfaceLock(k->ioInputs[0], kIOSurfaceLockReadOnly, NULL);
uint16_t *fp16_in = (uint16_t *)IOSurfaceGetBaseAddress(k->ioInputs[0]);
float *fp32_out = (float *)inputArray.dataPointer;
// fp16 → fp32 conversion
for (size_t i = 0; i < nElems; i++) {
// Simple fp16 decode (sufficient for benchmarking)
uint16_t h = fp16_in[i];
uint32_t sign = (h >> 15) & 1;
uint32_t exp = (h >> 10) & 0x1F;
uint32_t mant = h & 0x3FF;
uint32_t f;
if (exp == 0) {
f = sign << 31;
} else if (exp == 31) {
f = (sign << 31) | 0x7F800000 | (mant << 13);
} else {
f = (sign << 31) | ((exp + 112) << 23) | (mant << 13);
}
memcpy(&fp32_out[i], &f, 4);
}
IOSurfaceUnlock(k->ioInputs[0], kIOSurfaceLockReadOnly, NULL);
MLDictionaryFeatureProvider *provider = [[MLDictionaryFeatureProvider alloc]
initWithDictionary:@{k->coremlInputName: inputArray} error:&e];
if (!provider) return false;
id<MLFeatureProvider> result = [k->coremlModel predictionFromFeatures:provider error:&e];
if (!result) return false;
// Write output back to IOSurface
MLMultiArray *outArray = [result featureValueForName:k->coremlOutputName].multiArrayValue;
if (outArray) {
size_t outElems = k->outputBytes[0] / 2;
float *fp32_res = (float *)outArray.dataPointer;
IOSurfaceLock(k->ioOutputs[0], 0, NULL);
uint16_t *fp16_dst = (uint16_t *)IOSurfaceGetBaseAddress(k->ioOutputs[0]);
for (size_t i = 0; i < outElems && i < (size_t)outArray.count; i++) {
// fp32 → fp16 (simple truncation)
float val = fp32_res[i];
uint32_t f;
memcpy(&f, &val, 4);
uint32_t sign = (f >> 31) & 1;
int32_t exp = ((f >> 23) & 0xFF) - 127 + 15;
uint32_t mant = (f >> 13) & 0x3FF;
if (exp <= 0) fp16_dst[i] = sign << 15;
else if (exp >= 31) fp16_dst[i] = (sign << 15) | 0x7C00;
else fp16_dst[i] = (sign << 15) | (exp << 10) | mant;
}
IOSurfaceUnlock(k->ioOutputs[0], 0, NULL);
}
return true;
}
return false;
}
static void ane_universal_free(UANEKernel *k) {
if (!k) return;
NSError *e = nil;
if (k->backend == ANE_BACKEND_INMEM) {
((BOOL(*)(id,SEL,unsigned int,NSError**))objc_msgSend)(
k->inMemModel, @selector(unloadWithQoS:error:), 21, &e);
[[NSFileManager defaultManager] removeItemAtPath:k->tmpDir error:nil];
} else if (k->backend == ANE_BACKEND_COREML) {
if (k->coremlCompiledPath)
[[NSFileManager defaultManager] removeItemAtPath:k->coremlCompiledPath error:nil];
}
for (int i = 0; i < k->nInputs; i++) CFRelease(k->ioInputs[i]);
for (int i = 0; i < k->nOutputs; i++) CFRelease(k->ioOutputs[i]);
free(k->ioInputs);
free(k->ioOutputs);
free(k->inputBytes);
free(k->outputBytes);
free(k);
}
static const char *ane_backend_name(void) {
switch (g_ane_backend) {
case ANE_BACKEND_INMEM: return "InMemory (M4+)";
case ANE_BACKEND_COREML: return "CoreML (M1-M3)";
default: return "None";
}
}