diff --git a/objectivec/docs/jazzy_config.yaml b/objectivec/docs/jazzy_config.yaml new file mode 100644 index 0000000..700bb9b --- /dev/null +++ b/objectivec/docs/jazzy_config.yaml @@ -0,0 +1,15 @@ +module: ONNX Runtime Objective-C API +author: ONNX Runtime Authors +author_url: https://www.onnxruntime.ai +github_url: https://github.com/microsoft/onnxruntime + +objc: true +# Specify the training header as umbrella_header so that every public header is included. +# The training header is a superset of the inference-only header. +umbrella_header: ../include/onnxruntime_training.h +framework_root: .. + +readme: ./main_page.md + +hide_documentation_coverage: true +undocumented_text: "" diff --git a/objectivec/docs/main_page.md b/objectivec/docs/main_page.md new file mode 100644 index 0000000..fdc7c80 --- /dev/null +++ b/objectivec/docs/main_page.md @@ -0,0 +1,5 @@ +# ONNX Runtime Objective-C API + +ONNX Runtime provides an Objective-C API. + +It can be used from Objective-C/C++ or Swift with a bridging header. diff --git a/objectivec/docs/readme.md b/objectivec/docs/readme.md new file mode 100644 index 0000000..5697480 --- /dev/null +++ b/objectivec/docs/readme.md @@ -0,0 +1,17 @@ +# Objective-C API Documentation + +The API should be documented with comments in the [public header files](../include). + +## Documentation Generation + +The [Jazzy](https://github.com/realm/jazzy) tool is used to generate documentation from the code. + +To generate documentation, from the repo root, run: + +```bash +jazzy --config objectivec/docs/jazzy_config.yaml --output +``` + +The generated documentation website files will be in ``. + +[main_page.md](./main_page.md) contains content for the main page of the generated documentation website. diff --git a/objectivec/test/assert_arc_enabled.mm b/objectivec/test/assert_arc_enabled.mm new file mode 100644 index 0000000..9aa0bad --- /dev/null +++ b/objectivec/test/assert_arc_enabled.mm @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +static_assert(__has_feature(objc_arc), "Objective-C ARC must be enabled."); diff --git a/objectivec/test/assertion_utils.h b/objectivec/test/assertion_utils.h new file mode 100644 index 0000000..2b72435 --- /dev/null +++ b/objectivec/test/assertion_utils.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +#define ORTAssertNullableResultSuccessful(result, error) \ + do { \ + XCTAssertNotNil(result, @"Expected non-nil result but got nil. Error: %@", error); \ + XCTAssertNil(error); \ + } while (0) + +#define ORTAssertBoolResultSuccessful(result, error) \ + do { \ + XCTAssertTrue(result, @"Expected true result but got false. Error: %@", error); \ + XCTAssertNil(error); \ + } while (0) + +#define ORTAssertNullableResultUnsuccessful(result, error) \ + do { \ + XCTAssertNil(result); \ + XCTAssertNotNil(error); \ + } while (0) + +#define ORTAssertBoolResultUnsuccessful(result, error) \ + do { \ + XCTAssertFalse(result); \ + XCTAssertNotNil(error); \ + } while (0) + +#define ORTAssertEqualFloatAndNoError(expected, result, error) \ + do { \ + XCTAssertEqualWithAccuracy(expected, result, 1e-3f, @"Expected %f but got %f. Error:%@", expected, result, error); \ + XCTAssertNil(error); \ + } while (0) + +#define ORTAssertEqualFloatArrays(expected, result) \ + do { \ + XCTAssertEqual(expected.count, result.count); \ + for (size_t i = 0; i < expected.count; ++i) { \ + XCTAssertEqualWithAccuracy([expected[i] floatValue], [result[i] floatValue], 1e-3f); \ + } \ + } while (0) + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_checkpoint_test.mm b/objectivec/test/ort_checkpoint_test.mm new file mode 100644 index 0000000..9b2196f --- /dev/null +++ b/objectivec/test/ort_checkpoint_test.mm @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_checkpoint.h" +#import "ort_training_session.h" +#import "ort_env.h" +#import "ort_session.h" + +#import "test/test_utils.h" +#import "test/assertion_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTCheckpointTest : XCTestCase +@property(readonly, nullable) ORTEnv* ortEnv; +@end + +@implementation ORTCheckpointTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; + + NSError* err = nil; + _ortEnv = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning + error:&err]; + ORTAssertNullableResultSuccessful(_ortEnv, err); +} + ++ (NSString*)getCheckpointPath { + NSBundle* bundle = [NSBundle bundleForClass:[ORTCheckpointTest class]]; + NSString* path = [[bundle resourcePath] stringByAppendingPathComponent:@"checkpoint.ckpt"]; + return path; +} + ++ (NSString*)getTrainingModelPath { + NSBundle* bundle = [NSBundle bundleForClass:[ORTCheckpointTest class]]; + NSString* path = [[bundle resourcePath] stringByAppendingPathComponent:@"training_model.onnx"]; + return path; +} + +- (void)testSaveCheckpoint { + NSError* error = nil; + ORTCheckpoint* checkpoint = [[ORTCheckpoint alloc] initWithPath:[ORTCheckpointTest getCheckpointPath] error:&error]; + ORTAssertNullableResultSuccessful(checkpoint, error); + + // save checkpoint + NSString* path = [test_utils::createTemporaryDirectory(self) stringByAppendingPathComponent:@"save_checkpoint.ckpt"]; + XCTAssertNotNil(path); + BOOL result = [checkpoint saveCheckpointToPath:path withOptimizerState:NO error:&error]; + + ORTAssertBoolResultSuccessful(result, error); +} + +- (void)testInitCheckpoint { + NSError* error = nil; + ORTCheckpoint* checkpoint = [[ORTCheckpoint alloc] initWithPath:[ORTCheckpointTest getCheckpointPath] error:&error]; + ORTAssertNullableResultSuccessful(checkpoint, error); +} + +- (void)testIntProperty { + NSError* error = nil; + // Load checkpoint + ORTCheckpoint* checkpoint = [[ORTCheckpoint alloc] initWithPath:[ORTCheckpointTest getCheckpointPath] error:&error]; + ORTAssertNullableResultSuccessful(checkpoint, error); + + // Add property + BOOL result = [checkpoint addIntPropertyWithName:@"test" value:314 error:&error]; + ORTAssertBoolResultSuccessful(result, error); + + // Get property + int64_t value = [checkpoint getIntPropertyWithName:@"test" error:&error]; + XCTAssertEqual(value, 314); +} + +- (void)testFloatProperty { + NSError* error = nil; + // Load checkpoint + ORTCheckpoint* checkpoint = [[ORTCheckpoint alloc] initWithPath:[ORTCheckpointTest getCheckpointPath] error:&error]; + ORTAssertNullableResultSuccessful(checkpoint, error); + + // Add property + BOOL result = [checkpoint addFloatPropertyWithName:@"test" value:3.14f error:&error]; + ORTAssertBoolResultSuccessful(result, error); + + // Get property + float value = [checkpoint getFloatPropertyWithName:@"test" error:&error]; + XCTAssertEqual(value, 3.14f); +} + +- (void)testStringProperty { + NSError* error = nil; + // Load checkpoint + ORTCheckpoint* checkpoint = [[ORTCheckpoint alloc] initWithPath:[ORTCheckpointTest getCheckpointPath] error:&error]; + ORTAssertNullableResultSuccessful(checkpoint, error); + + // Add property + BOOL result = [checkpoint addStringPropertyWithName:@"test" value:@"hello" error:&error]; + ORTAssertBoolResultSuccessful(result, error); + + // Get property + NSString* value = [checkpoint getStringPropertyWithName:@"test" error:&error]; + XCTAssertEqualObjects(value, @"hello"); +} + +- (void)tearDown { + _ortEnv = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_env_test.mm b/objectivec/test/ort_env_test.mm new file mode 100644 index 0000000..420f55f --- /dev/null +++ b/objectivec/test/ort_env_test.mm @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_env.h" + +#import "test/assertion_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTEnvTest : XCTestCase +@end + +@implementation ORTEnvTest +- (void)testGetOrtVersion { + NSString* ver = ORTVersion(); + XCTAssertNotNil(ver); +} + +- (void)testInitOk { + NSError* err = nil; + ORTEnv* env = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning + error:&err]; + ORTAssertNullableResultSuccessful(env, err); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_session_test.mm b/objectivec/test/ort_session_test.mm new file mode 100644 index 0000000..508289f --- /dev/null +++ b/objectivec/test/ort_session_test.mm @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_coreml_execution_provider.h" +#import "ort_xnnpack_execution_provider.h" +#import "ort_env.h" +#import "ort_session.h" +#import "ort_value.h" + +#import "test/assertion_utils.h" + +#include + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTSessionTest : XCTestCase + +@property(readonly, nullable) ORTEnv* ortEnv; + +@end + +@implementation ORTSessionTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; + + NSError* err = nil; + _ortEnv = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning + error:&err]; + ORTAssertNullableResultSuccessful(_ortEnv, err); +} + +- (void)tearDown { + _ortEnv = nil; + + [super tearDown]; +} + +// model with an Add op +// inputs: A, B +// output: C = A + B ++ (NSString*)getAddModelPath { + NSBundle* bundle = [NSBundle bundleForClass:[ORTSessionTest class]]; + NSString* path = [bundle pathForResource:@"single_add.basic" + ofType:@"ort"]; + return path; +} + ++ (NSString*)getStringModelPath { + NSBundle* bundle = [NSBundle bundleForClass:[ORTSessionTest class]]; + NSString* path = [bundle pathForResource:@"identity_string" + ofType:@"ort"]; + return path; +} + ++ (NSMutableData*)dataWithScalarFloat:(float)value { + NSMutableData* data = [[NSMutableData alloc] initWithBytes:&value length:sizeof(value)]; + return data; +} + ++ (ORTValue*)ortValueWithScalarFloatData:(NSMutableData*)data { + NSArray* shape = @[ @1 ]; + NSError* err = nil; + ORTValue* ortValue = [[ORTValue alloc] initWithTensorData:data + elementType:ORTTensorElementDataTypeFloat + shape:shape + error:&err]; + ORTAssertNullableResultSuccessful(ortValue, err); + return ortValue; +} + ++ (ORTSessionOptions*)makeSessionOptions { + NSError* err = nil; + ORTSessionOptions* sessionOptions = [[ORTSessionOptions alloc] initWithError:&err]; + ORTAssertNullableResultSuccessful(sessionOptions, err); + return sessionOptions; +} + ++ (ORTRunOptions*)makeRunOptions { + NSError* err = nil; + ORTRunOptions* runOptions = [[ORTRunOptions alloc] initWithError:&err]; + ORTAssertNullableResultSuccessful(runOptions, err); + return runOptions; +} + +- (void)testInitAndRunWithPreallocatedOutputOk { + NSMutableData* aData = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* bData = [ORTSessionTest dataWithScalarFloat:2.0f]; + NSMutableData* cData = [ORTSessionTest dataWithScalarFloat:0.0f]; + + ORTValue* a = [ORTSessionTest ortValueWithScalarFloatData:aData]; + ORTValue* b = [ORTSessionTest ortValueWithScalarFloatData:bData]; + ORTValue* c = [ORTSessionTest ortValueWithScalarFloatData:cData]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + BOOL runResult = [session runWithInputs:@{@"A" : a, @"B" : b} + outputs:@{@"C" : c} + runOptions:[ORTSessionTest makeRunOptions] + error:&err]; + ORTAssertBoolResultSuccessful(runResult, err); + + const float cExpected = 3.0f; + float cActual; + memcpy(&cActual, cData.bytes, sizeof(float)); + XCTAssertEqual(cActual, cExpected); +} + +- (void)testInitAndRunOk { + NSMutableData* aData = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* bData = [ORTSessionTest dataWithScalarFloat:2.0f]; + + ORTValue* a = [ORTSessionTest ortValueWithScalarFloatData:aData]; + ORTValue* b = [ORTSessionTest ortValueWithScalarFloatData:bData]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + NSDictionary* outputs = + [session runWithInputs:@{@"A" : a, @"B" : b} + outputNames:[NSSet setWithArray:@[ @"C" ]] + runOptions:[ORTSessionTest makeRunOptions] + error:&err]; + ORTAssertNullableResultSuccessful(outputs, err); + + ORTValue* cOutput = outputs[@"C"]; + XCTAssertNotNil(cOutput); + + NSData* cData = [cOutput tensorDataWithError:&err]; + ORTAssertNullableResultSuccessful(cData, err); + + const float cExpected = 3.0f; + float cActual; + memcpy(&cActual, cData.bytes, sizeof(float)); + XCTAssertEqual(cActual, cExpected); +} + +- (void)testGetNamesOk { + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + NSArray* inputNames = [session inputNamesWithError:&err]; + ORTAssertNullableResultSuccessful(inputNames, err); + XCTAssertEqualObjects(inputNames, (@[ @"A", @"B" ])); + + NSArray* overridableInitializerNames = [session overridableInitializerNamesWithError:&err]; + ORTAssertNullableResultSuccessful(overridableInitializerNames, err); + XCTAssertEqualObjects(overridableInitializerNames, (@[])); + + NSArray* outputNames = [session outputNamesWithError:&err]; + ORTAssertNullableResultSuccessful(outputNames, err); + XCTAssertEqualObjects(outputNames, (@[ @"C" ])); +} + +- (void)testInitFailsWithInvalidPath { + NSString* invalidModelPath = @"invalid/path/to/model.ort"; + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:invalidModelPath + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultUnsuccessful(session, err); +} + +- (void)testRunFailsWithInvalidInput { + NSMutableData* dData = [ORTSessionTest dataWithScalarFloat:1.0f]; + NSMutableData* cData = [ORTSessionTest dataWithScalarFloat:0.0f]; + + ORTValue* d = [ORTSessionTest ortValueWithScalarFloatData:dData]; + ORTValue* c = [ORTSessionTest ortValueWithScalarFloatData:cData]; + + NSError* err = nil; + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + BOOL runResult = [session runWithInputs:@{@"D" : d} + outputs:@{@"C" : c} + runOptions:[ORTSessionTest makeRunOptions] + error:&err]; + ORTAssertBoolResultUnsuccessful(runResult, err); +} + +- (void)testAppendCoreMLEP { + NSError* err = nil; + ORTSessionOptions* sessionOptions = [ORTSessionTest makeSessionOptions]; + ORTCoreMLExecutionProviderOptions* coreMLOptions = [[ORTCoreMLExecutionProviderOptions alloc] init]; + coreMLOptions.enableOnSubgraphs = YES; // set an arbitrary option + + BOOL appendResult = [sessionOptions appendCoreMLExecutionProviderWithOptions:coreMLOptions + error:&err]; + + if (!ORTIsCoreMLExecutionProviderAvailable()) { + ORTAssertBoolResultUnsuccessful(appendResult, err); + return; + } + + ORTAssertBoolResultSuccessful(appendResult, err); + + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:sessionOptions + error:&err]; + ORTAssertNullableResultSuccessful(session, err); +} + +- (void)testAppendXnnpackEP { + NSError* err = nil; + ORTSessionOptions* sessionOptions = [ORTSessionTest makeSessionOptions]; + ORTXnnpackExecutionProviderOptions* XnnpackOptions = [[ORTXnnpackExecutionProviderOptions alloc] init]; + XnnpackOptions.intra_op_num_threads = 2; + + BOOL appendResult = [sessionOptions appendXnnpackExecutionProviderWithOptions:XnnpackOptions + error:&err]; + // Without xnnpack EP in building also can pass the test + NSString* err_msg = [err localizedDescription]; + if (!appendResult && [err_msg containsString:@"XNNPACK execution provider is not supported in this build. "]) { + return; + } + + ORTAssertBoolResultSuccessful(appendResult, err); + + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:sessionOptions + error:&err]; + ORTAssertNullableResultSuccessful(session, err); +} + +static bool gDummyRegisterCustomOpsFnCalled = false; + +static OrtStatus* _Nullable DummyRegisterCustomOpsFn(OrtSessionOptions* /*session_options*/, + const OrtApiBase* /*api*/) { + gDummyRegisterCustomOpsFnCalled = true; + return nullptr; +} + +- (void)testRegisterCustomOpsUsingFunctionPointer { + NSError* err = nil; + ORTSessionOptions* sessionOptions = [ORTSessionTest makeSessionOptions]; + + gDummyRegisterCustomOpsFnCalled = false; + BOOL registerResult = [sessionOptions registerCustomOpsUsingFunctionPointer:&DummyRegisterCustomOpsFn + error:&err]; + ORTAssertBoolResultSuccessful(registerResult, err); + + XCTAssertEqual(gDummyRegisterCustomOpsFnCalled, true); +} + +- (void)testStringInputs { + NSError* err = nil; + NSArray* stringData = @[ @"ONNX Runtime", @"is the", @"best", @"AI Framework" ]; + ORTValue* stringValue = [[ORTValue alloc] initWithTensorStringData:stringData shape:@[ @2, @2 ] error:&err]; + ORTAssertNullableResultSuccessful(stringValue, err); + + ORTSession* session = [[ORTSession alloc] initWithEnv:self.ortEnv + modelPath:[ORTSessionTest getStringModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + NSDictionary* outputs = + [session runWithInputs:@{@"input:0" : stringValue} + outputNames:[NSSet setWithArray:@[ @"output:0" ]] + runOptions:[ORTSessionTest makeRunOptions] + error:&err]; + ORTAssertNullableResultSuccessful(outputs, err); + + ORTValue* outputStringValue = outputs[@"output:0"]; + XCTAssertNotNil(outputStringValue); + + NSArray* outputStringData = [outputStringValue tensorStringDataWithError:&err]; + ORTAssertNullableResultSuccessful(outputStringData, err); + + XCTAssertEqual([stringData count], [outputStringData count]); + XCTAssertTrue([stringData isEqualToArray:outputStringData]); +} + +- (void)testKeepORTEnvReference { + ORTEnv* __weak envWeak = _ortEnv; + // Remove sole strong reference to the ORTEnv created in setUp. + _ortEnv = nil; + // There should be no more strong references to it. + XCTAssertNil(envWeak); + + // Create a new ORTEnv. + NSError* err = nil; + ORTEnv* env = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning + error:&err]; + ORTAssertNullableResultSuccessful(env, err); + + ORTSession* session = [[ORTSession alloc] initWithEnv:env + modelPath:[ORTSessionTest getAddModelPath] + sessionOptions:[ORTSessionTest makeSessionOptions] + error:&err]; + ORTAssertNullableResultSuccessful(session, err); + + envWeak = env; + // Remove strong reference to the ORTEnv passed to the ORTSession initializer. + env = nil; + // ORTSession should keep a strong reference to it. + XCTAssertNotNil(envWeak); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_training_session_test.mm b/objectivec/test/ort_training_session_test.mm new file mode 100644 index 0000000..683965d --- /dev/null +++ b/objectivec/test/ort_training_session_test.mm @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_checkpoint.h" +#import "ort_training_session.h" +#import "ort_env.h" +#import "ort_session.h" +#import "ort_value.h" + +#import "test/test_utils.h" +#import "test/assertion_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTTrainingSessionTest : XCTestCase +@property(readonly, nullable) ORTEnv* ortEnv; +@property(readonly, nullable) ORTCheckpoint* checkpoint; +@property(readonly, nullable) ORTTrainingSession* session; +@end + +@implementation ORTTrainingSessionTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; + + NSError* err = nil; + _ortEnv = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning + error:&err]; + ORTAssertNullableResultSuccessful(_ortEnv, err); + _checkpoint = [[ORTCheckpoint alloc] initWithPath:[ORTTrainingSessionTest + getFilePathFromName:@"checkpoint.ckpt"] + error:&err]; + ORTAssertNullableResultSuccessful(_checkpoint, err); + _session = [self makeTrainingSessionWithCheckpoint:_checkpoint]; +} + ++ (NSString*)getFilePathFromName:(NSString*)name { + NSBundle* bundle = [NSBundle bundleForClass:[ORTTrainingSessionTest class]]; + NSString* path = [[bundle resourcePath] stringByAppendingPathComponent:name]; + return path; +} + ++ (NSMutableData*)loadTensorDataFromFile:(NSString*)filePath skipHeader:(BOOL)skipHeader { + NSError* error = nil; + NSString* fileContents = [NSString stringWithContentsOfFile:filePath + encoding:NSUTF8StringEncoding + error:&error]; + ORTAssertNullableResultSuccessful(fileContents, error); + + NSArray* lines = [fileContents componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + + if (skipHeader) { + lines = [lines subarrayWithRange:NSMakeRange(1, lines.count - 1)]; + } + + NSArray* dataArray = [lines[0] componentsSeparatedByCharactersInSet: + [NSCharacterSet characterSetWithCharactersInString:@",[] "]]; + NSMutableData* tensorData = [NSMutableData data]; + + for (NSString* str in dataArray) { + if (str.length > 0) { + float value = [str floatValue]; + [tensorData appendBytes:&value length:sizeof(float)]; + } + } + + return tensorData; +} + +- (ORTTrainingSession*)makeTrainingSessionWithCheckpoint:(ORTCheckpoint*)checkpoint { + NSError* error = nil; + ORTSessionOptions* sessionOptions = [[ORTSessionOptions alloc] initWithError:&error]; + ORTAssertNullableResultSuccessful(sessionOptions, error); + + ORTTrainingSession* session = [[ORTTrainingSession alloc] + initWithEnv:self.ortEnv + sessionOptions:sessionOptions + checkpoint:checkpoint + trainModelPath:[ORTTrainingSessionTest getFilePathFromName:@"training_model.onnx"] + evalModelPath:[ORTTrainingSessionTest getFilePathFromName:@"eval_model.onnx"] + optimizerModelPath:[ORTTrainingSessionTest getFilePathFromName:@"adamw.onnx"] + error:&error]; + + ORTAssertNullableResultSuccessful(session, error); + return session; +} + +- (void)testInitTrainingSession { + NSError* error = nil; + + // check that inputNames contains input-0 + NSArray* inputNames = [self.session getTrainInputNamesWithError:&error]; + ORTAssertNullableResultSuccessful(inputNames, error); + + XCTAssertTrue(inputNames.count > 0); + XCTAssertTrue([inputNames containsObject:@"input-0"]); + + // check that outNames contains onnx::loss::21273 + NSArray* outputNames = [self.session getTrainOutputNamesWithError:&error]; + ORTAssertNullableResultSuccessful(outputNames, error); + + XCTAssertTrue(outputNames.count > 0); + XCTAssertTrue([outputNames containsObject:@"onnx::loss::21273"]); +} + +- (void)testInitTrainingSessionWithEval { + NSError* error = nil; + + // check that inputNames contains input-0 + NSArray* inputNames = [self.session getEvalInputNamesWithError:&error]; + ORTAssertNullableResultSuccessful(inputNames, error); + + XCTAssertTrue(inputNames.count > 0); + XCTAssertTrue([inputNames containsObject:@"input-0"]); + + // check that outNames contains onnx::loss::21273 + NSArray* outputNames = [self.session getEvalOutputNamesWithError:&error]; + ORTAssertNullableResultSuccessful(outputNames, error); + + XCTAssertTrue(outputNames.count > 0); + XCTAssertTrue([outputNames containsObject:@"onnx::loss::21273"]); +} + +- (void)runTrainStep { + // load input and expected output + NSError* error = nil; + NSMutableData* expectedOutput = [ORTTrainingSessionTest loadTensorDataFromFile:[ORTTrainingSessionTest + getFilePathFromName:@"loss_1.out"] + skipHeader:YES]; + + NSMutableData* input = [ORTTrainingSessionTest loadTensorDataFromFile:[ORTTrainingSessionTest + getFilePathFromName:@"input-0.in"] + skipHeader:YES]; + + int32_t labels[] = {1, 1}; + + // create ORTValue array for input and labels + NSMutableArray* inputValues = [NSMutableArray array]; + + ORTValue* inputTensor = [[ORTValue alloc] initWithTensorData:input + elementType:ORTTensorElementDataTypeFloat + shape:@[ @2, @784 ] + error:&error]; + ORTAssertNullableResultSuccessful(inputTensor, error); + [inputValues addObject:inputTensor]; + + ORTValue* labelTensor = [[ORTValue alloc] initWithTensorData:[NSMutableData dataWithBytes:labels + length:sizeof(labels)] + elementType:ORTTensorElementDataTypeInt32 + shape:@[ @2 ] + error:&error]; + + ORTAssertNullableResultSuccessful(labelTensor, error); + [inputValues addObject:labelTensor]; + + NSArray* outputs = [self.session trainStepWithInputValues:inputValues error:&error]; + ORTAssertNullableResultSuccessful(outputs, error); + XCTAssertTrue(outputs.count > 0); + + BOOL result = [self.session lazyResetGradWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + + outputs = [self.session trainStepWithInputValues:inputValues error:&error]; + ORTAssertNullableResultSuccessful(outputs, error); + XCTAssertTrue(outputs.count > 0); + + ORTValue* outputValue = outputs[0]; + ORTValueTypeInfo* typeInfo = [outputValue typeInfoWithError:&error]; + ORTAssertNullableResultSuccessful(typeInfo, error); + XCTAssertEqual(typeInfo.type, ORTValueTypeTensor); + XCTAssertNotNil(typeInfo.tensorTypeAndShapeInfo); + + ORTTensorTypeAndShapeInfo* tensorInfo = [outputValue tensorTypeAndShapeInfoWithError:&error]; + ORTAssertNullableResultSuccessful(tensorInfo, error); + XCTAssertEqual(tensorInfo.elementType, ORTTensorElementDataTypeFloat); + + NSMutableData* tensorData = [outputValue tensorDataWithError:&error]; + ORTAssertNullableResultSuccessful(tensorData, error); + ORTAssertEqualFloatArrays(test_utils::getFloatArrayFromData(tensorData), + test_utils::getFloatArrayFromData(expectedOutput)); +} + +- (void)testTrainStepOutput { + [self runTrainStep]; +} + +- (void)testOptimizerStep { + // load input and expected output + NSError* error = nil; + NSMutableData* expectedOutput1 = [ORTTrainingSessionTest loadTensorDataFromFile:[ORTTrainingSessionTest + getFilePathFromName:@"loss_1.out"] + skipHeader:YES]; + + NSMutableData* expectedOutput2 = [ORTTrainingSessionTest loadTensorDataFromFile:[ORTTrainingSessionTest + getFilePathFromName:@"loss_2.out"] + skipHeader:YES]; + + NSMutableData* input = [ORTTrainingSessionTest loadTensorDataFromFile:[ORTTrainingSessionTest + getFilePathFromName:@"input-0.in"] + skipHeader:YES]; + + int32_t labels[] = {1, 1}; + + // create ORTValue array for input and labels + NSMutableArray* inputValues = [NSMutableArray array]; + + ORTValue* inputTensor = [[ORTValue alloc] initWithTensorData:input + elementType:ORTTensorElementDataTypeFloat + shape:@[ @2, @784 ] + error:&error]; + ORTAssertNullableResultSuccessful(inputTensor, error); + [inputValues addObject:inputTensor]; + + ORTValue* labelTensor = [[ORTValue alloc] initWithTensorData:[NSMutableData dataWithBytes:labels + length:sizeof(labels)] + elementType:ORTTensorElementDataTypeInt32 + shape:@[ @2 ] + error:&error]; + ORTAssertNullableResultSuccessful(labelTensor, error); + [inputValues addObject:labelTensor]; + + // run train step, optimizer steps and check loss + NSArray* outputs = [self.session trainStepWithInputValues:inputValues error:&error]; + ORTAssertNullableResultSuccessful(outputs, error); + + NSMutableData* loss = [outputs[0] tensorDataWithError:&error]; + ORTAssertNullableResultSuccessful(loss, error); + ORTAssertEqualFloatArrays(test_utils::getFloatArrayFromData(loss), + test_utils::getFloatArrayFromData(expectedOutput1)); + + BOOL result = [self.session lazyResetGradWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + + outputs = [self.session trainStepWithInputValues:inputValues error:&error]; + ORTAssertNullableResultSuccessful(outputs, error); + + loss = [outputs[0] tensorDataWithError:&error]; + ORTAssertNullableResultSuccessful(loss, error); + ORTAssertEqualFloatArrays(test_utils::getFloatArrayFromData(loss), + test_utils::getFloatArrayFromData(expectedOutput1)); + + result = [self.session optimizerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + + outputs = [self.session trainStepWithInputValues:inputValues error:&error]; + ORTAssertNullableResultSuccessful(outputs, error); + loss = [outputs[0] tensorDataWithError:&error]; + ORTAssertNullableResultSuccessful(loss, error); + ORTAssertEqualFloatArrays(test_utils::getFloatArrayFromData(loss), + test_utils::getFloatArrayFromData(expectedOutput2)); +} + +- (void)testSetLearningRate { + NSError* error = nil; + + float learningRate = 0.1f; + BOOL result = [self.session setLearningRate:learningRate error:&error]; + ORTAssertBoolResultSuccessful(result, error); + + float actualLearningRate = [self.session getLearningRateWithError:&error]; + ORTAssertEqualFloatAndNoError(learningRate, actualLearningRate, error); +} + +- (void)testLinearLRScheduler { + NSError* error = nil; + + float learningRate = 0.1f; + BOOL result = [self.session registerLinearLRSchedulerWithWarmupStepCount:2 + totalStepCount:4 + initialLr:learningRate + error:&error]; + + ORTAssertBoolResultSuccessful(result, error); + + [self runTrainStep]; + + result = [self.session optimizerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + result = [self.session schedulerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + ORTAssertEqualFloatAndNoError(0.05f, [self.session getLearningRateWithError:&error], error); + + result = [self.session optimizerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + result = [self.session schedulerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + ORTAssertEqualFloatAndNoError(0.1f, [self.session getLearningRateWithError:&error], error); + + result = [self.session optimizerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + result = [self.session schedulerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + ORTAssertEqualFloatAndNoError(0.05f, [self.session getLearningRateWithError:&error], error); + + result = [self.session optimizerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + result = [self.session schedulerStepWithError:&error]; + ORTAssertBoolResultSuccessful(result, error); + ORTAssertEqualFloatAndNoError(0.0f, [self.session getLearningRateWithError:&error], error); +} + +- (void)testExportModelForInference { + NSError* error = nil; + + NSString* inferenceModelPath = [test_utils::createTemporaryDirectory(self) + stringByAppendingPathComponent:@"inference_model.onnx"]; + XCTAssertNotNil(inferenceModelPath); + + NSArray* graphOutputNames = [NSArray arrayWithObjects:@"output-0", nil]; + + BOOL result = [self.session exportModelForInferenceWithOutputPath:inferenceModelPath + graphOutputNames:graphOutputNames + error:&error]; + + ORTAssertBoolResultSuccessful(result, error); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:inferenceModelPath]); + + [self addTeardownBlock:^{ + NSError* error = nil; + [[NSFileManager defaultManager] removeItemAtPath:inferenceModelPath error:&error]; + }]; +} + +- (void)testToBuffer { + NSError* error = nil; + ORTValue* buffer = [self.session toBufferWithTrainable:YES error:&error]; + ORTAssertNullableResultSuccessful(buffer, error); + + ORTValueTypeInfo* typeInfo = [buffer typeInfoWithError:&error]; + ORTAssertNullableResultSuccessful(typeInfo, error); + XCTAssertEqual(typeInfo.type, ORTValueTypeTensor); + XCTAssertNotNil(typeInfo.tensorTypeAndShapeInfo); +} + +- (void)testFromBuffer { + NSError* error = nil; + + ORTValue* buffer = [self.session toBufferWithTrainable:YES error:&error]; + ORTAssertNullableResultSuccessful(buffer, error); + + BOOL result = [self.session fromBufferWithValue:buffer error:&error]; + ORTAssertBoolResultSuccessful(result, error); +} + +- (void)tearDown { + _session = nil; + _checkpoint = nil; + _ortEnv = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_training_utils_test.mm b/objectivec/test/ort_training_utils_test.mm new file mode 100644 index 0000000..1695ddc --- /dev/null +++ b/objectivec/test/ort_training_utils_test.mm @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#import "ort_training_session.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTTrainingUtilsTest : XCTestCase +@end + +@implementation ORTTrainingUtilsTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; +} + +- (void)testSetSeed { + ORTSetSeed(2718); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/ort_value_test.mm b/objectivec/test/ort_value_test.mm new file mode 100644 index 0000000..b22d73b --- /dev/null +++ b/objectivec/test/ort_value_test.mm @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "ort_value.h" + +#include + +#import "test/assertion_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ORTValueTest : XCTestCase +@end + +@implementation ORTValueTest + +- (void)setUp { + [super setUp]; + + self.continueAfterFailure = NO; +} + +- (void)testInitTensorOk { + int32_t value = 42; + NSMutableData* data = [[NSMutableData alloc] initWithBytes:&value + length:sizeof(int32_t)]; + NSArray* shape = @[ @1 ]; + + const ORTTensorElementDataType elementType = ORTTensorElementDataTypeInt32; + + NSError* err = nil; + ORTValue* ortValue = [[ORTValue alloc] initWithTensorData:data + elementType:elementType + shape:shape + error:&err]; + ORTAssertNullableResultSuccessful(ortValue, err); + + auto checkTensorInfo = [&](ORTTensorTypeAndShapeInfo* tensorInfo) { + XCTAssertEqual(tensorInfo.elementType, elementType); + XCTAssertEqualObjects(tensorInfo.shape, shape); + }; + + ORTValueTypeInfo* typeInfo = [ortValue typeInfoWithError:&err]; + ORTAssertNullableResultSuccessful(typeInfo, err); + XCTAssertEqual(typeInfo.type, ORTValueTypeTensor); + XCTAssertNotNil(typeInfo.tensorTypeAndShapeInfo); + checkTensorInfo(typeInfo.tensorTypeAndShapeInfo); + + ORTTensorTypeAndShapeInfo* tensorInfo = [ortValue tensorTypeAndShapeInfoWithError:&err]; + ORTAssertNullableResultSuccessful(tensorInfo, err); + checkTensorInfo(tensorInfo); + + NSData* actualData = [ortValue tensorDataWithError:&err]; + ORTAssertNullableResultSuccessful(actualData, err); + XCTAssertEqual(actualData.length, sizeof(int32_t)); + int32_t actualValue; + memcpy(&actualValue, actualData.bytes, sizeof(int32_t)); + XCTAssertEqual(actualValue, value); +} + +- (void)testInitTensorFailsWithDataSmallerThanShape { + std::vector values{1, 2, 3, 4}; + NSMutableData* data = [[NSMutableData alloc] initWithBytes:values.data() + length:values.size() * sizeof(int32_t)]; + NSArray* shape = @[ @2, @3 ]; // too large + + NSError* err = nil; + ORTValue* ortValue = [[ORTValue alloc] initWithTensorData:data + elementType:ORTTensorElementDataTypeInt32 + shape:shape + error:&err]; + ORTAssertNullableResultUnsuccessful(ortValue, err); +} + +- (void)testInitTensorWithStringDataSucceeds { + NSArray* stringData = @[ @"ONNX Runtime", @"is", @"the", @"best", @"AI", @"Framework" ]; + NSError* err = nil; + ORTValue* stringValue = [[ORTValue alloc] initWithTensorStringData:stringData shape:@[ @3, @2 ] error:&err]; + ORTAssertNullableResultSuccessful(stringValue, err); + + NSArray* returnedStringData = [stringValue tensorStringDataWithError:&err]; + ORTAssertNullableResultSuccessful(returnedStringData, err); + + XCTAssertEqual([stringData count], [returnedStringData count]); + XCTAssertTrue([stringData isEqualToArray:returnedStringData]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/test_utils.h b/objectivec/test/test_utils.h new file mode 100644 index 0000000..8a5e6e4 --- /dev/null +++ b/objectivec/test/test_utils.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#import + +#import "ort_session.h" +#import "ort_env.h" +#import "ort_value.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace test_utils { + +NSString* _Nullable createTemporaryDirectory(XCTestCase* testCase); + +NSArray* getFloatArrayFromData(NSData* data); + +} // namespace test_utils + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/test_utils.mm b/objectivec/test/test_utils.mm new file mode 100644 index 0000000..ef8e8c5 --- /dev/null +++ b/objectivec/test/test_utils.mm @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "test_utils.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace test_utils { + +NSString* createTemporaryDirectory(XCTestCase* testCase) { + NSString* temporaryDirectory = NSTemporaryDirectory(); + NSString* directoryPath = [temporaryDirectory stringByAppendingPathComponent:@"ort-objective-c-test"]; + + NSError* error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:directoryPath + withIntermediateDirectories:YES + attributes:nil + error:&error]; + + XCTAssertNil(error, @"Error creating temporary directory: %@", error.localizedDescription); + + // add teardown block to delete the temporary directory + [testCase addTeardownBlock:^{ + NSError* error = nil; + [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:&error]; + XCTAssertNil(error, @"Error removing temporary directory: %@", error.localizedDescription); + }]; + + return directoryPath; +} + +NSArray* getFloatArrayFromData(NSData* data) { + NSMutableArray* array = [NSMutableArray array]; + float value; + for (size_t i = 0; i < data.length / sizeof(float); ++i) { + [data getBytes:&value range:NSMakeRange(i * sizeof(float), sizeof(float))]; + [array addObject:[NSNumber numberWithFloat:value]]; + } + return array; +} + +} // namespace test_utils + +NS_ASSUME_NONNULL_END diff --git a/objectivec/test/testdata/gen_models.sh b/objectivec/test/testdata/gen_models.sh new file mode 100644 index 0000000..0a58ca8 --- /dev/null +++ b/objectivec/test/testdata/gen_models.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +# Get directory this script is in +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +cd ${DIR} + +python3 ./single_add_gen.py + +ORT_CONVERT_ONNX_MODELS_TO_ORT_OPTIMIZATION_LEVEL=basic python3 -m onnxruntime.tools.convert_onnx_models_to_ort --optimization_style=Fixed . diff --git a/objectivec/test/testdata/identity_string.ort b/objectivec/test/testdata/identity_string.ort new file mode 100644 index 0000000..96ba0f3 Binary files /dev/null and b/objectivec/test/testdata/identity_string.ort differ diff --git a/objectivec/test/testdata/single_add.basic.ort b/objectivec/test/testdata/single_add.basic.ort new file mode 100644 index 0000000..f622784 Binary files /dev/null and b/objectivec/test/testdata/single_add.basic.ort differ diff --git a/objectivec/test/testdata/single_add.onnx b/objectivec/test/testdata/single_add.onnx new file mode 100644 index 0000000..82e8fe6 Binary files /dev/null and b/objectivec/test/testdata/single_add.onnx differ diff --git a/objectivec/test/testdata/single_add_gen.py b/objectivec/test/testdata/single_add_gen.py new file mode 100644 index 0000000..15b4372 --- /dev/null +++ b/objectivec/test/testdata/single_add_gen.py @@ -0,0 +1,19 @@ +import onnx +from onnx import TensorProto, helper + +graph = helper.make_graph( + [ # nodes + helper.make_node("Add", ["A", "B"], ["C"], "Add"), + ], + "SingleAdd", # name + [ # inputs + helper.make_tensor_value_info("A", TensorProto.FLOAT, [1]), + helper.make_tensor_value_info("B", TensorProto.FLOAT, [1]), + ], + [ # outputs + helper.make_tensor_value_info("C", TensorProto.FLOAT, [1]), + ], +) + +model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 12)]) +onnx.save(model, r"single_add.onnx")