diff --git a/Package.swift b/Package.swift index 08842c2..477ac49 100644 --- a/Package.swift +++ b/Package.swift @@ -1,114 +1,35 @@ -// swift-tools-version: 5.9 - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// A user of the Swift Package Manager (SPM) package will consume this file directly from the ORT SPM github repository. -// For example, the end user's config will look something like: -// -// dependencies: [ -// .package(url: "https://github.com/microsoft/onnxruntime-swift-package-manager", from: "1.16.0"), -// ... -// ], -// -// NOTE: For valid version numbers, please refer to this page: -// https://github.com/microsoft/onnxruntime-swift-package-manager/releases +// swift-tools-version: 6.1 import PackageDescription -import class Foundation.ProcessInfo let package = Package( name: "onnxruntime", - platforms: [.iOS(.v15), - .macOS(.v14)], products: [ - .library(name: "onnxruntime", - type: .static, - targets: ["OnnxRuntimeBindings"]), - .library(name: "onnxruntime_extensions", - type: .static, - targets: ["OnnxRuntimeExtensions"]), + .library( + name: "onnxruntime", + targets: ["OnnxRuntimeBindings"] + ), ], - dependencies: [], targets: [ - .target(name: "OnnxRuntimeBindings", - dependencies: ["onnxruntime"], - path: "objectivec", - exclude: ["ReadMe.md", "format_objc.sh", "test", "docs", - "ort_checkpoint.mm", - "ort_checkpoint_internal.h", - "ort_training_session_internal.h", - "ort_training_session.mm", - "include/ort_checkpoint.h", - "include/ort_training_session.h", - "include/onnxruntime_training.h"], - cxxSettings: [ - .define("SPM_BUILD"), - ]), - .testTarget(name: "OnnxRuntimeBindingsTests", - dependencies: ["OnnxRuntimeBindings"], - path: "swift/OnnxRuntimeBindingsTests", - resources: [ - .copy("Resources/single_add.basic.ort") - ]), - .target(name: "OnnxRuntimeExtensions", - dependencies: ["onnxruntime_extensions", "onnxruntime"], - path: "extensions", - cxxSettings: [ - .define("ORT_SWIFT_PACKAGE_MANAGER_BUILD"), - ]), - .testTarget(name: "OnnxRuntimeExtensionsTests", - dependencies: ["OnnxRuntimeExtensions", "OnnxRuntimeBindings"], - path: "swift/OnnxRuntimeExtensionsTests", - resources: [ - .copy("Resources/decode_image.onnx") - ]), + .systemLibrary( + name: "COnnxRuntime", + path: "swift/COnnxRuntime", + pkgConfig: "libonnxruntime" + ), + .target( + name: "OnnxRuntimeBindings", + dependencies: ["COnnxRuntime"], + path: "swift/OnnxRuntimeBindings" + ), + .testTarget( + name: "OnnxRuntimeBindingsTests", + dependencies: ["OnnxRuntimeBindings", "COnnxRuntime"], + path: "swift/OnnxRuntimeBindingsTests", + exclude: ["SwiftOnnxRuntimeBindingsTests.swift"], + resources: [ + .copy("Resources/single_add.basic.ort") + ] + ), ], - cxxLanguageStandard: .cxx17 + cxxLanguageStandard: .cxx20 ) - -// Add the ORT CocoaPods C/C++ pod archive as a binary target. -// -// There are 2 scenarios: -// - Target will be set to a released pod archive and its checksum. -// -// - Target will be set to a local pod archive. -// This can be used to test with the latest (not yet released) ORT Objective-C source code. - -// CI or local testing where you have built/obtained the pod archive matching the current source code. -// Requires the ORT_POD_LOCAL_PATH environment variable to be set to specify the location of the pod. -if let pod_archive_path = ProcessInfo.processInfo.environment["ORT_POD_LOCAL_PATH"] { - // ORT_POD_LOCAL_PATH MUST be a path that is relative to Package.swift. - // - // To build locally, tools/ci_build/github/apple/build_and_assemble_apple_pods.py can be used - // See https://onnxruntime.ai/docs/build/custom.html#ios - // Example command: - // python3 tools/ci_build/github/apple/build_and_assemble_apple_pods.py \ - // --build-settings-file tools/ci_build/github/apple/default_full_apple_framework_build_settings.json - // - // This should produce the pod archive in build/apple_pod_staging, and ORT_POD_LOCAL_PATH can be set to - // "build/apple_pod_staging/pod-archive-onnxruntime-c-???.zip" where '???' is replaced by the version info in the - // actual filename. - package.targets.append(Target.binaryTarget(name: "onnxruntime", path: pod_archive_path)) - -} else { - // ORT release - package.targets.append( - Target.binaryTarget(name: "onnxruntime", - url: "https://download.onnxruntime.ai/pod-archive-onnxruntime-c-1.24.2.zip", - // SHA256 checksum - checksum: "f7100a992d2a8135168c8afd831e6a58b465349101982aa58b3e11d36e600b54") - ) -} - -if let ext_pod_archive_path = ProcessInfo.processInfo.environment["ORT_EXTENSIONS_POD_LOCAL_PATH"] { - package.targets.append(Target.binaryTarget(name: "onnxruntime_extensions", path: ext_pod_archive_path)) -} else { - // ORT Extensions release - package.targets.append( - Target.binaryTarget(name: "onnxruntime_extensions", - url: "https://download.onnxruntime.ai/pod-archive-onnxruntime-extensions-c-0.13.0.zip", - // SHA256 checksum - checksum: "346522d1171d4c99cb0908fa8e4e9330a4a6aad39cd83ce36eb654437b33e6b5") - ) -} diff --git a/README.md b/README.md index 156ece7..a4741f3 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,48 @@ # Swift Package Manager for ONNX Runtime -A light-weight repository for providing [Swift Package Manager (SPM)](https://www.swift.org/package-manager/) support for [ONNXRuntime](https://github.com/microsoft/onnxruntime). The ONNX Runtime native package is included as a binary dependency of the SPM package. +Swift-native ONNX Runtime bindings derived from the public Objective-C wrapper surface, but implemented without the Objective-C bridge so the package can build on Linux. +## Status -SPM is the alternative to CocoaPods when desired platform to consume is mobile iOS. +- Public API covers inference, session/run options, tensor values, checkpoint state, and training session entry points. +- Backend is wired to the ONNX Runtime C API and training C API through a Swift system library target. +- The package expects `libonnxruntime` and headers to be available on the host, typically via `pkg-config`. -## Note +## Requirements -The `objectivec/` directory is copied from the [ORT repo](https://github.com/microsoft/onnxruntime/tree/main/objectivec) and it's expected to match. It will be updated periodically/before release to merge new changes. +- Swift 6.1+ +- ONNX Runtime installed on the system with: + - `libonnxruntime` + - `onnxruntime_c_api.h` + - `onnxruntime_training_c_api.h` + - `pkg-config` metadata for `libonnxruntime` -## Contributing +## Usage -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. +```swift +import OnnxRuntimeBindings -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. +let environment = try ORTEnv(loggingLevel: .warning) +let sessionOptions = try ORTSessionOptions() +let session = try ORTSession( + env: environment, + modelPath: "/path/to/model.onnx", + sessionOptions: sessionOptions +) -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +let inputTensor = try ORTValue( + tensorData: NSMutableData(data: inputData), + elementType: .float, + shape: [1, 224, 224, 3] +) -## Trademarks +let outputs = try session.run( + inputs: ["input": inputTensor], + outputNames: ["output"] +) +``` -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow -[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. +## Notes + +- The `objectivec/` directory remains in the repo as the reference contract surface from upstream ONNX Runtime. +- The shipped Swift package target is the pure Swift `OnnxRuntimeBindings` implementation under `swift/OnnxRuntimeBindings`. diff --git a/swift/COnnxRuntime/module.modulemap b/swift/COnnxRuntime/module.modulemap new file mode 100644 index 0000000..5c488bf --- /dev/null +++ b/swift/COnnxRuntime/module.modulemap @@ -0,0 +1,5 @@ +module COnnxRuntime [system] { + header "shim.h" + link "onnxruntime" + export * +} diff --git a/swift/COnnxRuntime/shim.h b/swift/COnnxRuntime/shim.h new file mode 100644 index 0000000..fe59624 --- /dev/null +++ b/swift/COnnxRuntime/shim.h @@ -0,0 +1,2 @@ +#include +#include diff --git a/swift/OnnxRuntimeBindings/ORTError.swift b/swift/OnnxRuntimeBindings/ORTError.swift new file mode 100644 index 0000000..e7cb192 --- /dev/null +++ b/swift/OnnxRuntimeBindings/ORTError.swift @@ -0,0 +1,21 @@ +import Foundation + +public enum ORTError: Error, LocalizedError { + case notImplemented(String) + case runtimeFailure(action: String, message: String) + case invalidArgument(String) + case unsupported(String) + + public var errorDescription: String? { + switch self { + case .notImplemented(let symbol): + return "ONNX Runtime Swift interface symbol is not implemented yet: \(symbol)" + case .runtimeFailure(let action, let message): + return "\(action) failed: \(message)" + case .invalidArgument(let message): + return message + case .unsupported(let message): + return message + } + } +} diff --git a/swift/OnnxRuntimeBindings/ORTInterface.swift b/swift/OnnxRuntimeBindings/ORTInterface.swift new file mode 100644 index 0000000..a4cc823 --- /dev/null +++ b/swift/OnnxRuntimeBindings/ORTInterface.swift @@ -0,0 +1,1280 @@ +@preconcurrency import COnnxRuntime +import Foundation + +public enum ORTLoggingLevel: Int32 { + case verbose + case info + case warning + case error + case fatal + + internal var native: OrtLoggingLevel { + OrtLoggingLevel(UInt32(rawValue)) + } +} + +public enum ORTValueType: Int32 { + case unknown + case tensor + + internal init(native: ONNXType) throws { + switch native { + case ONNX_TYPE_UNKNOWN: + self = .unknown + case ONNX_TYPE_TENSOR: + self = .tensor + default: + throw ORTError.unsupported("Only tensor ONNX values are currently supported") + } + } +} + +public enum ORTTensorElementDataType: Int32 { + case undefined + case float + case int8 + case uInt8 + case int32 + case uInt32 + case int64 + case uInt64 + case string + + internal var native: ONNXTensorElementDataType { + switch self { + case .undefined: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED + case .float: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT + case .int8: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8 + case .uInt8: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8 + case .int32: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32 + case .uInt32: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32 + case .int64: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64 + case .uInt64: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64 + case .string: + return ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING + } + } + + internal init(native: ONNXTensorElementDataType) throws { + switch native { + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED: + self = .undefined + case ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT: + self = .float + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8: + self = .int8 + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8: + self = .uInt8 + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32: + self = .int32 + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32: + self = .uInt32 + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64: + self = .int64 + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64: + self = .uInt64 + case ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING: + self = .string + default: + throw ORTError.unsupported("Unsupported tensor element type \(native.rawValue)") + } + } +} + +public enum ORTGraphOptimizationLevel: Int32 { + case none + case basic + case extended + case layout + case all + + internal var native: GraphOptimizationLevel { + switch self { + case .none: + return ORT_DISABLE_ALL + case .basic: + return ORT_ENABLE_BASIC + case .extended: + return ORT_ENABLE_EXTENDED + case .layout, .all: + return ORT_ENABLE_ALL + } + } +} + +public typealias ORTCAPIRegisterCustomOpsFnPtr = UnsafeMutableRawPointer + +private final class ORTValueStorage { + let handle: OpaquePointer + let retainedBuffer: NSMutableData? + + init(handle: OpaquePointer, retainedBuffer: NSMutableData? = nil) { + self.handle = handle + self.retainedBuffer = retainedBuffer + } + + deinit { + ORTNative.api.pointee.ReleaseValue?(handle) + } +} + +public func ORTVersion() -> String? { + ORTNative.apiBase?.pointee.GetVersionString().map { String(cString: $0) } +} + +public func ORTIsCoreMLExecutionProviderAvailable() -> Bool { + false +} + +public func ORTSetSeed(_ seed: Int64) { + guard let trainingApi = ORTNative.trainingApi else { + return + } + + _ = trainingApi.pointee.SetSeed?(seed) +} + +public struct ORTValueTypeInfo { + public let type: ORTValueType + public let tensorTypeAndShapeInfo: ORTTensorTypeAndShapeInfo? + + public init(type: ORTValueType, tensorTypeAndShapeInfo: ORTTensorTypeAndShapeInfo?) { + self.type = type + self.tensorTypeAndShapeInfo = tensorTypeAndShapeInfo + } +} + +public struct ORTTensorTypeAndShapeInfo { + public let elementType: ORTTensorElementDataType + public let shape: [Int] + + public init(elementType: ORTTensorElementDataType, shape: [Int]) { + self.elementType = elementType + self.shape = shape + } +} + +public struct ORTCoreMLExecutionProviderOptions { + public var useCPUOnly: Bool + public var useCPUAndGPU: Bool + public var enableOnSubgraphs: Bool + public var onlyEnableForDevicesWithANE: Bool + public var onlyAllowStaticInputShapes: Bool + public var createMLProgram: Bool + + public init( + useCPUOnly: Bool = false, + useCPUAndGPU: Bool = false, + enableOnSubgraphs: Bool = false, + onlyEnableForDevicesWithANE: Bool = false, + onlyAllowStaticInputShapes: Bool = false, + createMLProgram: Bool = false + ) { + self.useCPUOnly = useCPUOnly + self.useCPUAndGPU = useCPUAndGPU + self.enableOnSubgraphs = enableOnSubgraphs + self.onlyEnableForDevicesWithANE = onlyEnableForDevicesWithANE + self.onlyAllowStaticInputShapes = onlyAllowStaticInputShapes + self.createMLProgram = createMLProgram + } + + internal var providerOptions: [String: String] { + [ + "use_cpu_only": ORTNative.boolString(useCPUOnly), + "use_cpu_and_gpu": ORTNative.boolString(useCPUAndGPU), + "enable_on_subgraphs": ORTNative.boolString(enableOnSubgraphs), + "only_enable_for_devices_with_ane": ORTNative.boolString(onlyEnableForDevicesWithANE), + "only_allow_static_input_shapes": ORTNative.boolString(onlyAllowStaticInputShapes), + "create_mlprogram": ORTNative.boolString(createMLProgram), + ] + } +} + +public struct ORTXnnpackExecutionProviderOptions { + public var intraOpNumThreads: Int32 + + public init(intraOpNumThreads: Int32 = 0) { + self.intraOpNumThreads = intraOpNumThreads + } +} + +public final class ORTEnv { + public let loggingLevel: ORTLoggingLevel + internal let handle: OpaquePointer + + public init(loggingLevel: ORTLoggingLevel) throws { + self.loggingLevel = loggingLevel + + var env: OpaquePointer? + try ORTNative.check( + ORTNative.api.pointee.CreateEnv?(loggingLevel.native, "onnxruntime-swift-interface", &env), + action: "CreateEnv" + ) + guard let env else { + throw ORTError.runtimeFailure(action: "CreateEnv", message: "Environment was not returned") + } + handle = env + } + + deinit { + ORTNative.api.pointee.ReleaseEnv?(handle) + } +} + +public final class ORTSession { + private let handle: OpaquePointer + + public init( + env: ORTEnv, + modelPath: String, + sessionOptions: ORTSessionOptions? = nil + ) throws { + var session: OpaquePointer? + let createdOptions = sessionOptions == nil ? try ORTSessionOptions() : nil + let optionsHandle = sessionOptions?.handle ?? createdOptions!.handle + + try modelPath.withCString { modelPathPointer in + try ORTNative.check( + ORTNative.api.pointee.CreateSession?(env.handle, modelPathPointer, optionsHandle, &session), + action: "CreateSession" + ) + } + + guard let session else { + throw ORTError.runtimeFailure(action: "CreateSession", message: "Session was not returned") + } + handle = session + } + + deinit { + ORTNative.api.pointee.ReleaseSession?(handle) + } + + public func run( + inputs: [String: ORTValue], + outputs: [String: ORTValue], + runOptions: ORTRunOptions? = nil + ) throws { + let sortedInputs = inputs.keys.sorted() + let sortedOutputs = outputs.keys.sorted() + let inputValues = sortedInputs.map { Optional(inputs[$0]!.handle) } + var outputValues = sortedOutputs.map { Optional(outputs[$0]!.handle) } + let outputCount = outputValues.count + + try ORTNative.withCStringArray(sortedInputs) { inputNames in + try ORTNative.withCStringArray(sortedOutputs) { outputNames in + try inputNames.withUnsafeBufferPointer { inputNameBuffer in + try outputNames.withUnsafeBufferPointer { outputNameBuffer in + try inputValues.withUnsafeBufferPointer { inputValueBuffer in + try outputValues.withUnsafeMutableBufferPointer { outputValueBuffer in + try ORTNative.check( + ORTNative.api.pointee.Run?( + handle, + runOptions?.handle, + inputNameBuffer.baseAddress, + inputValueBuffer.baseAddress, + inputValues.count, + outputNameBuffer.baseAddress, + outputCount, + outputValueBuffer.baseAddress + ), + action: "Run" + ) + } + } + } + } + } + } + } + + public func run( + inputs: [String: ORTValue], + outputNames: Set, + runOptions: ORTRunOptions? = nil + ) throws -> [String: ORTValue] { + let sortedInputs = inputs.keys.sorted() + let sortedOutputs = outputNames.sorted() + + let inputValues = sortedInputs.map { Optional(inputs[$0]!.handle) } + var outputValues = Array(repeating: nil, count: sortedOutputs.count) + let outputCount = sortedOutputs.count + + try ORTNative.withCStringArray(sortedInputs) { inputNames in + try ORTNative.withCStringArray(sortedOutputs) { outputNamesArray in + try inputNames.withUnsafeBufferPointer { inputNameBuffer in + try outputNamesArray.withUnsafeBufferPointer { outputNameBuffer in + try inputValues.withUnsafeBufferPointer { inputValueBuffer in + try outputValues.withUnsafeMutableBufferPointer { outputValueBuffer in + try ORTNative.check( + ORTNative.api.pointee.Run?( + handle, + runOptions?.handle, + inputNameBuffer.baseAddress, + inputValueBuffer.baseAddress, + inputValues.count, + outputNameBuffer.baseAddress, + outputCount, + outputValueBuffer.baseAddress + ), + action: "Run" + ) + } + } + } + } + } + } + + var results: [String: ORTValue] = [:] + for (index, name) in sortedOutputs.enumerated() { + guard let valueHandle = outputValues[index] else { + throw ORTError.runtimeFailure(action: "Run", message: "Output \(name) was not returned") + } + results[name] = ORTValue(adopting: valueHandle) + } + return results + } + + public func inputNames() throws -> [String] { + var count = 0 + try ORTNative.check(ORTNative.api.pointee.SessionGetInputCount?(handle, &count), action: "SessionGetInputCount") + return try ORTNative.withAllocator { allocator in + var results: [String] = [] + results.reserveCapacity(count) + for index in 0..? + try ORTNative.check( + ORTNative.api.pointee.SessionGetInputName?(handle, index, allocator, &pointer), + action: "SessionGetInputName" + ) + results.append(try ORTNative.copyAllocatedString(pointer, allocator: allocator)) + } + return results + } + } + + public func overridableInitializerNames() throws -> [String] { + var count = 0 + try ORTNative.check( + ORTNative.api.pointee.SessionGetOverridableInitializerCount?(handle, &count), + action: "SessionGetOverridableInitializerCount" + ) + return try ORTNative.withAllocator { allocator in + var results: [String] = [] + results.reserveCapacity(count) + for index in 0..? + try ORTNative.check( + ORTNative.api.pointee.SessionGetOverridableInitializerName?(handle, index, allocator, &pointer), + action: "SessionGetOverridableInitializerName" + ) + results.append(try ORTNative.copyAllocatedString(pointer, allocator: allocator)) + } + return results + } + } + + public func outputNames() throws -> [String] { + var count = 0 + try ORTNative.check(ORTNative.api.pointee.SessionGetOutputCount?(handle, &count), action: "SessionGetOutputCount") + return try ORTNative.withAllocator { allocator in + var results: [String] = [] + results.reserveCapacity(count) + for index in 0..? + try ORTNative.check( + ORTNative.api.pointee.SessionGetOutputName?(handle, index, allocator, &pointer), + action: "SessionGetOutputName" + ) + results.append(try ORTNative.copyAllocatedString(pointer, allocator: allocator)) + } + return results + } + } +} + +public final class ORTSessionOptions { + public private(set) var executionProviders: [(name: String, options: [String: String])] = [] + public private(set) var intraOpNumThreads: Int32 = 0 + public private(set) var graphOptimizationLevel: ORTGraphOptimizationLevel = .all + public private(set) var optimizedModelFilePath: String? + public private(set) var logID: String? + public private(set) var logSeverityLevel: ORTLoggingLevel? + public private(set) var configEntries: [String: String] = [:] + public private(set) var customOpRegistrationFunctionName: String? + public private(set) var customOpRegistrationFunctionPointer: ORTCAPIRegisterCustomOpsFnPtr? + public private(set) var ortExtensionsCustomOpsEnabled = false + public private(set) var coreMLExecutionProviderOptions: ORTCoreMLExecutionProviderOptions? + public private(set) var coreMLExecutionProviderV2Options: [String: String]? + public private(set) var xnnpackExecutionProviderOptions: ORTXnnpackExecutionProviderOptions? + + internal let handle: OpaquePointer + + public init() throws { + var options: OpaquePointer? + try ORTNative.check(ORTNative.api.pointee.CreateSessionOptions?(&options), action: "CreateSessionOptions") + guard let options else { + throw ORTError.runtimeFailure(action: "CreateSessionOptions", message: "Session options were not returned") + } + handle = options + } + + deinit { + ORTNative.api.pointee.ReleaseSessionOptions?(handle) + } + + public func appendExecutionProvider( + _ providerName: String, + providerOptions: [String: String] + ) throws { + let sorted = providerOptions.keys.sorted().map { ($0, providerOptions[$0]!) } + let keyStorage = sorted.map { Array($0.0.utf8CString) } + let valueStorage = sorted.map { Array($0.1.utf8CString) } + let keys = keyStorage.map { $0.withUnsafeBufferPointer { $0.baseAddress } } + let values = valueStorage.map { $0.withUnsafeBufferPointer { $0.baseAddress } } + + try providerName.withCString { providerNamePointer in + try keys.withUnsafeBufferPointer { keyBuffer in + try values.withUnsafeBufferPointer { valueBuffer in + try ORTNative.check( + ORTNative.api.pointee.SessionOptionsAppendExecutionProvider?( + handle, + providerNamePointer, + keyBuffer.baseAddress, + valueBuffer.baseAddress, + sorted.count + ), + action: "SessionOptionsAppendExecutionProvider" + ) + } + } + } + + executionProviders.append((providerName, providerOptions)) + } + + public func setIntraOpNumThreads(_ count: Int32) throws { + try ORTNative.check( + ORTNative.api.pointee.SetIntraOpNumThreads?(handle, Int32(count)), + action: "SetIntraOpNumThreads" + ) + intraOpNumThreads = count + } + + public func setGraphOptimizationLevel(_ level: ORTGraphOptimizationLevel) throws { + try ORTNative.check( + ORTNative.api.pointee.SetSessionGraphOptimizationLevel?(handle, level.native), + action: "SetSessionGraphOptimizationLevel" + ) + graphOptimizationLevel = level + } + + public func setOptimizedModelFilePath(_ path: String) throws { + try path.withCString { pointer in + try ORTNative.check( + ORTNative.api.pointee.SetOptimizedModelFilePath?(handle, pointer), + action: "SetOptimizedModelFilePath" + ) + } + optimizedModelFilePath = path + } + + public func setLogID(_ logID: String) throws { + try logID.withCString { pointer in + try ORTNative.check( + ORTNative.api.pointee.SetSessionLogId?(handle, pointer), + action: "SetSessionLogId" + ) + } + self.logID = logID + } + + public func setLogSeverityLevel(_ level: ORTLoggingLevel) throws { + try ORTNative.check( + ORTNative.api.pointee.SetSessionLogSeverityLevel?(handle, level.rawValue), + action: "SetSessionLogSeverityLevel" + ) + logSeverityLevel = level + } + + public func addConfigEntry(key: String, value: String) throws { + try key.withCString { keyPointer in + try value.withCString { valuePointer in + try ORTNative.check( + ORTNative.api.pointee.AddSessionConfigEntry?(handle, keyPointer, valuePointer), + action: "AddSessionConfigEntry" + ) + } + } + configEntries[key] = value + } + + public func registerCustomOps(usingFunction name: String) throws { + try name.withCString { pointer in + try ORTNative.check( + ORTNative.api.pointee.RegisterCustomOpsUsingFunction?(handle, pointer), + action: "RegisterCustomOpsUsingFunction" + ) + } + customOpRegistrationFunctionName = name + } + + public func registerCustomOps(usingFunctionPointer fn: ORTCAPIRegisterCustomOpsFnPtr) throws { + let nativeFunction = unsafeBitCast(fn, to: RegisterCustomOpsFn.self) + try ORTNative.check( + nativeFunction(handle, ORTNative.apiBase), + action: "RegisterCustomOpsUsingFunctionPointer" + ) + customOpRegistrationFunctionPointer = fn + } + + public func enableOrtExtensionsCustomOps() throws { + try ORTNative.check( + ORTNative.api.pointee.EnableOrtCustomOps?(handle), + action: "EnableOrtCustomOps" + ) + ortExtensionsCustomOpsEnabled = true + } + + public func appendCoreMLExecutionProvider(options: ORTCoreMLExecutionProviderOptions) throws { + try appendCoreMLExecutionProviderV2(providerOptions: options.providerOptions) + coreMLExecutionProviderOptions = options + } + + public func appendCoreMLExecutionProviderV2(providerOptions: [String: String]) throws { + try appendExecutionProvider("CoreML", providerOptions: providerOptions) + coreMLExecutionProviderV2Options = providerOptions + } + + public func appendXnnpackExecutionProvider(options: ORTXnnpackExecutionProviderOptions) throws { + try appendExecutionProvider( + "XNNPACK", + providerOptions: ["intra_op_num_threads": String(options.intraOpNumThreads)] + ) + xnnpackExecutionProviderOptions = options + } +} + +public final class ORTRunOptions { + public private(set) var logTag: String? + public private(set) var logSeverityLevel: ORTLoggingLevel? + public private(set) var configEntries: [String: String] = [:] + + internal let handle: OpaquePointer + + public init() throws { + var options: OpaquePointer? + try ORTNative.check(ORTNative.api.pointee.CreateRunOptions?(&options), action: "CreateRunOptions") + guard let options else { + throw ORTError.runtimeFailure(action: "CreateRunOptions", message: "Run options were not returned") + } + handle = options + } + + deinit { + ORTNative.api.pointee.ReleaseRunOptions?(handle) + } + + public func setLogTag(_ tag: String) throws { + try tag.withCString { pointer in + try ORTNative.check( + ORTNative.api.pointee.RunOptionsSetRunTag?(handle, pointer), + action: "RunOptionsSetRunTag" + ) + } + logTag = tag + } + + public func setLogSeverityLevel(_ level: ORTLoggingLevel) throws { + try ORTNative.check( + ORTNative.api.pointee.RunOptionsSetRunLogSeverityLevel?(handle, level.rawValue), + action: "RunOptionsSetRunLogSeverityLevel" + ) + logSeverityLevel = level + } + + public func addConfigEntry(key: String, value: String) throws { + try key.withCString { keyPointer in + try value.withCString { valuePointer in + try ORTNative.check( + ORTNative.api.pointee.AddRunConfigEntry?(handle, keyPointer, valuePointer), + action: "AddRunConfigEntry" + ) + } + } + configEntries[key] = value + } +} + +public final class ORTValue { + private let storage: ORTValueStorage + + internal var handle: OpaquePointer { + storage.handle + } + + public init( + tensorData: NSMutableData, + elementType: ORTTensorElementDataType, + shape: [Int] + ) throws { + let elementCount = try ORTNative.tensorElementCount(shape: shape) + let expectedLength = try ORTNative.elementByteCount(for: elementType) * elementCount + guard tensorData.length >= expectedLength else { + throw ORTError.invalidArgument("Tensor buffer is too small for shape \(shape) and element type \(elementType)") + } + + var memoryInfo: OpaquePointer? + try ORTNative.check( + ORTNative.api.pointee.CreateCpuMemoryInfo?(OrtArenaAllocator, OrtMemTypeDefault, &memoryInfo), + action: "CreateCpuMemoryInfo" + ) + guard let memoryInfo else { + throw ORTError.runtimeFailure(action: "CreateCpuMemoryInfo", message: "Memory info was not returned") + } + defer { + ORTNative.api.pointee.ReleaseMemoryInfo?(memoryInfo) + } + + var value: OpaquePointer? + var nativeShape = ORTNative.makeShapeBuffer(shape) + let nativeShapeCount = nativeShape.count + try nativeShape.withUnsafeMutableBufferPointer { shapeBuffer in + try ORTNative.check( + ORTNative.api.pointee.CreateTensorWithDataAsOrtValue?( + memoryInfo, + tensorData.mutableBytes, + tensorData.length, + shapeBuffer.baseAddress, + nativeShapeCount, + elementType.native, + &value + ), + action: "CreateTensorWithDataAsOrtValue" + ) + } + + guard let value else { + throw ORTError.runtimeFailure(action: "CreateTensorWithDataAsOrtValue", message: "Value was not returned") + } + storage = ORTValueStorage(handle: value, retainedBuffer: tensorData) + } + + public init( + tensorStringData: [String], + shape: [Int] + ) throws { + let elementCount = try ORTNative.tensorElementCount(shape: shape) + guard elementCount == tensorStringData.count else { + throw ORTError.invalidArgument("String tensor element count does not match shape") + } + + var value: OpaquePointer? + var nativeShape = ORTNative.makeShapeBuffer(shape) + let nativeShapeCount = nativeShape.count + try ORTNative.withAllocator { allocator in + try nativeShape.withUnsafeMutableBufferPointer { shapeBuffer in + try ORTNative.check( + ORTNative.api.pointee.CreateTensorAsOrtValue?( + allocator, + shapeBuffer.baseAddress, + nativeShapeCount, + ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING, + &value + ), + action: "CreateTensorAsOrtValue" + ) + } + } + + guard let value else { + throw ORTError.runtimeFailure(action: "CreateTensorAsOrtValue", message: "String tensor value was not returned") + } + + let stringStorage = tensorStringData.map { Array($0.utf8CString) } + let stringPointers = stringStorage.map { $0.withUnsafeBufferPointer { $0.baseAddress } } + try stringPointers.withUnsafeBufferPointer { pointerBuffer in + try ORTNative.check( + ORTNative.api.pointee.FillStringTensor?(value, pointerBuffer.baseAddress, stringPointers.count), + action: "FillStringTensor" + ) + } + + storage = ORTValueStorage(handle: value) + } + + internal init(adopting handle: OpaquePointer) { + storage = ORTValueStorage(handle: handle) + } + + public func typeInfo() throws -> ORTValueTypeInfo { + try ORTNative.fetchValueTypeInfo(from: handle) + } + + public func tensorTypeAndShapeInfo() throws -> ORTTensorTypeAndShapeInfo { + try ORTNative.fetchTensorInfo(from: handle) + } + + public func tensorData() throws -> NSMutableData { + let tensorInfo = try tensorTypeAndShapeInfo() + guard tensorInfo.elementType != .string else { + throw ORTError.unsupported("tensorData() is not available for string tensors") + } + + var dataPointer: UnsafeMutableRawPointer? + try ORTNative.check( + ORTNative.api.pointee.GetTensorMutableData?(handle, &dataPointer), + action: "GetTensorMutableData" + ) + guard let dataPointer else { + throw ORTError.runtimeFailure(action: "GetTensorMutableData", message: "Tensor data pointer was not returned") + } + + let elementCount = try ORTNative.tensorElementCount(shape: tensorInfo.shape) + let length = try ORTNative.elementByteCount(for: tensorInfo.elementType) * elementCount + return NSMutableData(bytesNoCopy: dataPointer, length: length, freeWhenDone: false) + } + + public func tensorStringData() throws -> [String] { + let tensorInfo = try tensorTypeAndShapeInfo() + guard tensorInfo.elementType == .string else { + throw ORTError.unsupported("tensorStringData() is only available for string tensors") + } + + let elementCount = try ORTNative.tensorElementCount(shape: tensorInfo.shape) + var totalLength = 0 + try ORTNative.check( + ORTNative.api.pointee.GetStringTensorDataLength?(handle, &totalLength), + action: "GetStringTensorDataLength" + ) + + var content = Array(repeating: UInt8(0), count: totalLength) + var offsets = Array(repeating: Int(0), count: elementCount) + try ORTNative.check( + ORTNative.api.pointee.GetStringTensorContent?( + handle, + &content, + totalLength, + &offsets, + elementCount + ), + action: "GetStringTensorContent" + ) + + return (0.. Int64 { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + + return try ORTNative.withAllocator { allocator in + var propertyType = OrtPropertyType(rawValue: 0) + var propertyValue: UnsafeMutableRawPointer? + try name.withCString { pointer in + try ORTNative.check( + trainingApi.pointee.GetProperty?(handle, pointer, allocator, &propertyType, &propertyValue), + action: "GetProperty" + ) + } + + defer { + if let propertyValue { + _ = ORTNative.api.pointee.AllocatorFree?(allocator, propertyValue) + } + } + + guard propertyType == OrtIntProperty, let propertyValue else { + throw ORTError.invalidArgument("Checkpoint property \(name) is not an Int64") + } + return propertyValue.assumingMemoryBound(to: Int64.self).pointee + } + } + + public func floatProperty(name: String) throws -> Float { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + + return try ORTNative.withAllocator { allocator in + var propertyType = OrtPropertyType(rawValue: 0) + var propertyValue: UnsafeMutableRawPointer? + try name.withCString { pointer in + try ORTNative.check( + trainingApi.pointee.GetProperty?(handle, pointer, allocator, &propertyType, &propertyValue), + action: "GetProperty" + ) + } + + defer { + if let propertyValue { + _ = ORTNative.api.pointee.AllocatorFree?(allocator, propertyValue) + } + } + + guard propertyType == OrtFloatProperty, let propertyValue else { + throw ORTError.invalidArgument("Checkpoint property \(name) is not a Float") + } + return propertyValue.assumingMemoryBound(to: Float.self).pointee + } + } + + public func stringProperty(name: String) throws -> String? { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + + return try ORTNative.withAllocator { allocator in + var propertyType = OrtPropertyType(rawValue: 0) + var propertyValue: UnsafeMutableRawPointer? + try name.withCString { pointer in + try ORTNative.check( + trainingApi.pointee.GetProperty?(handle, pointer, allocator, &propertyType, &propertyValue), + action: "GetProperty" + ) + } + + guard propertyType == OrtStringProperty else { + if let propertyValue { + _ = ORTNative.api.pointee.AllocatorFree?(allocator, propertyValue) + } + throw ORTError.invalidArgument("Checkpoint property \(name) is not a String") + } + + let string = propertyValue.map { String(cString: $0.assumingMemoryBound(to: CChar.self)) } + if let propertyValue { + _ = ORTNative.api.pointee.AllocatorFree?(allocator, propertyValue) + } + return string + } + } +} + +public final class ORTTrainingSession { + private let handle: OpaquePointer + private let checkpoint: ORTCheckpoint + + public init( + env: ORTEnv, + sessionOptions: ORTSessionOptions? = nil, + checkpoint: ORTCheckpoint, + trainModelPath: String, + evalModelPath: String? = nil, + optimizerModelPath: String? = nil + ) throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + + self.checkpoint = checkpoint + + let createdOptions = sessionOptions == nil ? try ORTSessionOptions() : nil + let optionsHandle = sessionOptions?.handle ?? createdOptions!.handle + var trainingSession: OpaquePointer? + + try trainModelPath.withCString { trainPointer in + try evalModelPath?.withCString { evalPointer in + try optimizerModelPath?.withCString { optimizerPointer in + try ORTNative.check( + trainingApi.pointee.CreateTrainingSession?( + env.handle, + optionsHandle, + checkpoint.handle, + trainPointer, + evalPointer, + optimizerPointer, + &trainingSession + ), + action: "CreateTrainingSession" + ) + } + } ?? { + try optimizerModelPath?.withCString { optimizerPointer in + try ORTNative.check( + trainingApi.pointee.CreateTrainingSession?( + env.handle, + optionsHandle, + checkpoint.handle, + trainPointer, + nil, + optimizerPointer, + &trainingSession + ), + action: "CreateTrainingSession" + ) + } ?? { + try ORTNative.check( + trainingApi.pointee.CreateTrainingSession?( + env.handle, + optionsHandle, + checkpoint.handle, + trainPointer, + nil, + nil, + &trainingSession + ), + action: "CreateTrainingSession" + ) + }() + }() + } + + guard let trainingSession else { + throw ORTError.runtimeFailure(action: "CreateTrainingSession", message: "Training session was not returned") + } + handle = trainingSession + } + + deinit { + ORTNative.trainingApi?.pointee.ReleaseTrainingSession?(handle) + } + + public func trainStep(inputValues: [ORTValue]) throws -> [ORTValue] { + try runStep( + countAction: "TrainingSessionGetTrainingModelOutputCount", + countCall: { out in ORTNative.trainingApi!.pointee.TrainingSessionGetTrainingModelOutputCount?(handle, out) }, + stepAction: "TrainStep", + stepCall: { outputCount, outputBuffer in + let inputs = inputValues.map(\.handle) + let inputPointers = inputs.map(Optional.init) + return try inputPointers.withUnsafeBufferPointer { inputBuffer in + try ORTNative.check( + ORTNative.trainingApi!.pointee.TrainStep?( + handle, + nil, + inputs.count, + inputBuffer.baseAddress, + outputCount, + outputBuffer + ), + action: "TrainStep" + ) + } + } + ) + } + + public func evalStep(inputValues: [ORTValue]) throws -> [ORTValue] { + try runStep( + countAction: "TrainingSessionGetEvalModelOutputCount", + countCall: { out in ORTNative.trainingApi!.pointee.TrainingSessionGetEvalModelOutputCount?(handle, out) }, + stepAction: "EvalStep", + stepCall: { outputCount, outputBuffer in + let inputs = inputValues.map(\.handle) + let inputPointers = inputs.map(Optional.init) + return try inputPointers.withUnsafeBufferPointer { inputBuffer in + try ORTNative.check( + ORTNative.trainingApi!.pointee.EvalStep?( + handle, + nil, + inputs.count, + inputBuffer.baseAddress, + outputCount, + outputBuffer + ), + action: "EvalStep" + ) + } + } + ) + } + + public func lazyResetGrad() throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + try ORTNative.check(trainingApi.pointee.LazyResetGrad?(handle), action: "LazyResetGrad") + } + + public func optimizerStep() throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + try ORTNative.check(trainingApi.pointee.OptimizerStep?(handle, nil), action: "OptimizerStep") + } + + public func trainInputNames() throws -> [String] { + try trainingNames( + countAction: "TrainingSessionGetTrainingModelInputCount", + countCall: { out in ORTNative.trainingApi!.pointee.TrainingSessionGetTrainingModelInputCount?(handle, out) }, + nameAction: "TrainingSessionGetTrainingModelInputName", + nameCall: { index, allocator, out in + ORTNative.trainingApi!.pointee.TrainingSessionGetTrainingModelInputName?(handle, index, allocator, out) + } + ) + } + + public func evalInputNames() throws -> [String] { + try trainingNames( + countAction: "TrainingSessionGetEvalModelInputCount", + countCall: { out in ORTNative.trainingApi!.pointee.TrainingSessionGetEvalModelInputCount?(handle, out) }, + nameAction: "TrainingSessionGetEvalModelInputName", + nameCall: { index, allocator, out in + ORTNative.trainingApi!.pointee.TrainingSessionGetEvalModelInputName?(handle, index, allocator, out) + } + ) + } + + public func trainOutputNames() throws -> [String] { + try trainingNames( + countAction: "TrainingSessionGetTrainingModelOutputCount", + countCall: { out in ORTNative.trainingApi!.pointee.TrainingSessionGetTrainingModelOutputCount?(handle, out) }, + nameAction: "TrainingSessionGetTrainingModelOutputName", + nameCall: { index, allocator, out in + ORTNative.trainingApi!.pointee.TrainingSessionGetTrainingModelOutputName?(handle, index, allocator, out) + } + ) + } + + public func evalOutputNames() throws -> [String] { + try trainingNames( + countAction: "TrainingSessionGetEvalModelOutputCount", + countCall: { out in ORTNative.trainingApi!.pointee.TrainingSessionGetEvalModelOutputCount?(handle, out) }, + nameAction: "TrainingSessionGetEvalModelOutputName", + nameCall: { index, allocator, out in + ORTNative.trainingApi!.pointee.TrainingSessionGetEvalModelOutputName?(handle, index, allocator, out) + } + ) + } + + public func registerLinearLRScheduler( + warmupStepCount: Int64, + totalStepCount: Int64, + initialLr: Float + ) throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + try ORTNative.check( + trainingApi.pointee.RegisterLinearLRScheduler?(handle, warmupStepCount, totalStepCount, initialLr), + action: "RegisterLinearLRScheduler" + ) + } + + public func schedulerStep() throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + try ORTNative.check(trainingApi.pointee.SchedulerStep?(handle), action: "SchedulerStep") + } + + public func learningRate() throws -> Float { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + var learningRate: Float = 0 + try ORTNative.check(trainingApi.pointee.GetLearningRate?(handle, &learningRate), action: "GetLearningRate") + return learningRate + } + + public func setLearningRate(_ lr: Float) throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + try ORTNative.check(trainingApi.pointee.SetLearningRate?(handle, lr), action: "SetLearningRate") + } + + public func fromBuffer(_ buffer: ORTValue) throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + try ORTNative.check(trainingApi.pointee.CopyBufferToParameters?(handle, buffer.handle, false), action: "CopyBufferToParameters") + } + + public func toBuffer(onlyTrainable: Bool) throws -> ORTValue { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + + var parameterCount = 0 + try ORTNative.check( + trainingApi.pointee.GetParametersSize?(handle, ¶meterCount, onlyTrainable), + action: "GetParametersSize" + ) + + let byteCount = parameterCount * MemoryLayout.stride + let buffer = NSMutableData(length: byteCount) ?? NSMutableData() + let value = try ORTValue(tensorData: buffer, elementType: .float, shape: [parameterCount]) + try ORTNative.check( + trainingApi.pointee.CopyParametersToBuffer?(handle, value.handle, onlyTrainable), + action: "CopyParametersToBuffer" + ) + return value + } + + public func exportModelForInference( + outputPath: String, + graphOutputNames: [String] + ) throws { + guard let trainingApi = ORTNative.trainingApi else { + throw ORTError.unsupported("This ONNX Runtime build does not expose the training API") + } + + try outputPath.withCString { pathPointer in + try ORTNative.withCStringArray(graphOutputNames) { namePointers in + try namePointers.withUnsafeBufferPointer { nameBuffer in + try ORTNative.check( + trainingApi.pointee.ExportModelForInferencing?( + handle, + pathPointer, + graphOutputNames.count, + nameBuffer.baseAddress + ), + action: "ExportModelForInferencing" + ) + } + } + } + } + + private func trainingNames( + countAction: String, + countCall: (_ out: UnsafeMutablePointer) -> OpaquePointer?, + nameAction: String, + nameCall: ( + _ index: Int, + _ allocator: UnsafeMutablePointer, + _ out: UnsafeMutablePointer?> + ) -> OpaquePointer? + ) throws -> [String] { + var count = 0 + try ORTNative.check(countCall(&count), action: countAction) + + return try ORTNative.withAllocator { allocator in + var names: [String] = [] + names.reserveCapacity(count) + for index in 0..? + try ORTNative.check(nameCall(index, allocator, &namePointer), action: nameAction) + names.append(try ORTNative.copyAllocatedString(namePointer, allocator: allocator)) + } + return names + } + } + + private func runStep( + countAction: String, + countCall: (_ out: UnsafeMutablePointer) -> OpaquePointer?, + stepAction: String, + stepCall: (_ outputCount: Int, _ outputBuffer: UnsafeMutablePointer?) throws -> Void + ) throws -> [ORTValue] { + var outputCount = 0 + try ORTNative.check(countCall(&outputCount), action: countAction) + + var outputs = Array(repeating: nil, count: outputCount) + try outputs.withUnsafeMutableBufferPointer { buffer in + try stepCall(outputCount, buffer.baseAddress) + } + + return try outputs.enumerated().map { index, handle in + guard let handle else { + throw ORTError.runtimeFailure(action: stepAction, message: "Output \(index) was not returned") + } + return ORTValue(adopting: handle) + } + } +} diff --git a/swift/OnnxRuntimeBindings/ORTNative.swift b/swift/OnnxRuntimeBindings/ORTNative.swift new file mode 100644 index 0000000..73bba8d --- /dev/null +++ b/swift/OnnxRuntimeBindings/ORTNative.swift @@ -0,0 +1,166 @@ +@preconcurrency import COnnxRuntime +import Foundation + +internal enum ORTNative { + nonisolated(unsafe) static let apiBase = OrtGetApiBase() + nonisolated(unsafe) static let api = apiBase!.pointee.GetApi(UInt32(ORT_API_VERSION))! + nonisolated(unsafe) static let trainingApi = api.pointee.GetTrainingApi?(UInt32(ORT_API_VERSION)) + + static func check(_ status: OpaquePointer?, action: String) throws { + guard let status else { + return + } + + let message = api.pointee.GetErrorMessage?(status).map { String(cString: $0) } ?? "Unknown ONNX Runtime error" + api.pointee.ReleaseStatus?(status) + throw ORTError.runtimeFailure(action: action, message: message) + } + + static func withAllocator(_ body: (UnsafeMutablePointer) throws -> Result) throws -> Result { + var allocator: UnsafeMutablePointer? + try check(api.pointee.GetAllocatorWithDefaultOptions?(&allocator), action: "GetAllocatorWithDefaultOptions") + guard let allocator else { + throw ORTError.runtimeFailure(action: "GetAllocatorWithDefaultOptions", message: "Allocator was not returned") + } + return try body(allocator) + } + + static func copyAllocatedString( + _ stringPointer: UnsafeMutablePointer?, + allocator: UnsafeMutablePointer + ) throws -> String { + guard let stringPointer else { + throw ORTError.runtimeFailure(action: "copyAllocatedString", message: "Expected allocated string") + } + + defer { + _ = api.pointee.AllocatorFree?(allocator, stringPointer) + } + + return String(cString: stringPointer) + } + + static func tensorElementCount(shape: [Int]) throws -> Int { + guard shape.allSatisfy({ $0 >= 0 }) else { + throw ORTError.invalidArgument("Tensor shape contains a negative dimension: \(shape)") + } + + return shape.reduce(1, *) + } + + static func elementByteCount(for type: ORTTensorElementDataType) throws -> Int { + switch type { + case .float: + return MemoryLayout.stride + case .int8: + return MemoryLayout.stride + case .uInt8: + return MemoryLayout.stride + case .int32: + return MemoryLayout.stride + case .uInt32: + return MemoryLayout.stride + case .int64: + return MemoryLayout.stride + case .uInt64: + return MemoryLayout.stride + case .string: + throw ORTError.invalidArgument("String tensors do not have a fixed byte width") + case .undefined: + throw ORTError.invalidArgument("Undefined tensor element type is not supported") + } + } + + static func makeShapeBuffer(_ shape: [Int]) -> [Int64] { + shape.map(Int64.init) + } + + static func withCStringArray( + _ strings: [String], + _ body: ([UnsafePointer?]) throws -> Result + ) rethrows -> Result { + let storage = strings.map { Array($0.utf8CString) } + let pointers = storage.map { $0.withUnsafeBufferPointer { $0.baseAddress } } + return try body(pointers) + } + + static func fetchTensorInfo(from value: OpaquePointer) throws -> ORTTensorTypeAndShapeInfo { + var info: OpaquePointer? + try check(api.pointee.GetTensorTypeAndShape?(value, &info), action: "GetTensorTypeAndShape") + guard let info else { + throw ORTError.runtimeFailure(action: "GetTensorTypeAndShape", message: "Tensor info was not returned") + } + + defer { + api.pointee.ReleaseTensorTypeAndShapeInfo?(info) + } + + var elementType = ONNXTensorElementDataType(rawValue: 0) + try check(api.pointee.GetTensorElementType?(info, &elementType), action: "GetTensorElementType") + + var dimensionsCount: Int = 0 + try check(api.pointee.GetDimensionsCount?(info, &dimensionsCount), action: "GetDimensionsCount") + + var dimensions = Array(repeating: Int64(0), count: dimensionsCount) + if dimensionsCount > 0 { + try check( + api.pointee.GetDimensions?(info, &dimensions, dimensionsCount), + action: "GetDimensions" + ) + } + + return ORTTensorTypeAndShapeInfo( + elementType: try ORTTensorElementDataType(native: elementType), + shape: dimensions.map(Int.init) + ) + } + + static func fetchValueTypeInfo(from value: OpaquePointer) throws -> ORTValueTypeInfo { + var typeInfo: OpaquePointer? + try check(api.pointee.GetTypeInfo?(value, &typeInfo), action: "GetTypeInfo") + guard let typeInfo else { + throw ORTError.runtimeFailure(action: "GetTypeInfo", message: "Type info was not returned") + } + + defer { + api.pointee.ReleaseTypeInfo?(typeInfo) + } + + var onnxType = ONNXType(rawValue: 0) + try check(api.pointee.GetOnnxTypeFromTypeInfo?(typeInfo, &onnxType), action: "GetOnnxTypeFromTypeInfo") + + var tensorInfoPointer: OpaquePointer? + try check(api.pointee.CastTypeInfoToTensorInfo?(typeInfo, &tensorInfoPointer), action: "CastTypeInfoToTensorInfo") + + let tensorInfo = try tensorInfoPointer.map { pointer in + try fetchTensorInfoView(from: pointer) + } + + return ORTValueTypeInfo( + type: try ORTValueType(native: onnxType), + tensorTypeAndShapeInfo: tensorInfo + ) + } + + static func fetchTensorInfoView(from info: OpaquePointer) throws -> ORTTensorTypeAndShapeInfo { + var elementType = ONNXTensorElementDataType(rawValue: 0) + try check(api.pointee.GetTensorElementType?(info, &elementType), action: "GetTensorElementType") + + var dimensionsCount: Int = 0 + try check(api.pointee.GetDimensionsCount?(info, &dimensionsCount), action: "GetDimensionsCount") + + var dimensions = Array(repeating: Int64(0), count: dimensionsCount) + if dimensionsCount > 0 { + try check(api.pointee.GetDimensions?(info, &dimensions, dimensionsCount), action: "GetDimensions") + } + + return ORTTensorTypeAndShapeInfo( + elementType: try ORTTensorElementDataType(native: elementType), + shape: dimensions.map(Int.init) + ) + } + + static func boolString(_ value: Bool) -> String { + value ? "1" : "0" + } +} diff --git a/swift/OnnxRuntimeBindingsTests/SwiftAPITests.swift b/swift/OnnxRuntimeBindingsTests/SwiftAPITests.swift new file mode 100644 index 0000000..16ab28d --- /dev/null +++ b/swift/OnnxRuntimeBindingsTests/SwiftAPITests.swift @@ -0,0 +1,113 @@ +import Foundation +import Testing +@preconcurrency import COnnxRuntime +@testable import OnnxRuntimeBindings + +@_cdecl("SwiftTestRegisterCustomOps") +func SwiftTestRegisterCustomOps(_ options: OpaquePointer?, _ api: UnsafePointer?) -> OpaquePointer? { + _ = options + _ = api + return nil +} + +struct SwiftAPITests { + @Test + func sessionOptionsRetainConfiguredState() throws { + let options = try ORTSessionOptions() + + try options.setIntraOpNumThreads(4) + try options.setGraphOptimizationLevel(.extended) + try options.setOptimizedModelFilePath("/tmp/model.optimized.onnx") + try options.setLogID("session-log") + try options.setLogSeverityLevel(.warning) + try options.addConfigEntry(key: "session.disable_prepacking", value: "1") + + #expect(options.intraOpNumThreads == 4) + #expect(options.graphOptimizationLevel == .extended) + #expect(options.optimizedModelFilePath == "/tmp/model.optimized.onnx") + #expect(options.logID == "session-log") + #expect(options.logSeverityLevel == .warning) + #expect(options.configEntries["session.disable_prepacking"] == "1") + #expect(options.executionProviders.isEmpty) + } + + @Test + func unsupportedExecutionProviderSurfacesRuntimeError() throws { + let options = try ORTSessionOptions() + + #expect(throws: ORTError.self) { + try options.appendExecutionProvider("XNNPACK", providerOptions: ["threads": "2"]) + } + } + + @Test + func customOpsFunctionPointerRegistrationIsInvoked() throws { + let options = try ORTSessionOptions() + let functionPointer = unsafeBitCast( + SwiftTestRegisterCustomOps as RegisterCustomOpsFn, + to: UnsafeMutableRawPointer.self + ) + + try options.registerCustomOps(usingFunctionPointer: functionPointer) + + #expect(options.customOpRegistrationFunctionPointer == functionPointer) + } + + @Test + func runOptionsRetainConfiguredState() throws { + let options = try ORTRunOptions() + + try options.setLogTag("inference") + try options.setLogSeverityLevel(.error) + try options.addConfigEntry(key: "terminate", value: "0") + + #expect(options.logTag == "inference") + #expect(options.logSeverityLevel == .error) + #expect(options.configEntries["terminate"] == "0") + } + + @Test + func numericTensorValueExposesTensorInfo() throws { + let bytes = NSMutableData(data: Data([0, 0, 128, 63])) + let value = try ORTValue(tensorData: bytes, elementType: .float, shape: [1]) + + let typeInfo = try value.typeInfo() + let tensorInfo = try value.tensorTypeAndShapeInfo() + let data = try value.tensorData() + + #expect(typeInfo.type == .tensor) + #expect(typeInfo.tensorTypeAndShapeInfo?.elementType == .float) + #expect(tensorInfo.elementType == .float) + #expect(tensorInfo.shape == [1]) + #expect(data.length == bytes.length) + #expect(Data(referencing: data) == Data(referencing: bytes)) + } + + @Test + func stringTensorValueExposesTensorInfo() throws { + let value = try ORTValue(tensorStringData: ["hello", "world"], shape: [2]) + + let typeInfo = try value.typeInfo() + let tensorInfo = try value.tensorTypeAndShapeInfo() + let strings = try value.tensorStringData() + + #expect(typeInfo.type == .tensor) + #expect(typeInfo.tensorTypeAndShapeInfo?.elementType == .string) + #expect(tensorInfo.elementType == .string) + #expect(tensorInfo.shape == [2]) + #expect(strings == ["hello", "world"]) + } + + @Test + func wrongTensorAccessorThrows() throws { + let numeric = try ORTValue(tensorData: NSMutableData(data: Data([0, 0, 0, 0])), elementType: .float, shape: []) + let string = try ORTValue(tensorStringData: [], shape: [0]) + + #expect(throws: ORTError.self) { + _ = try numeric.tensorStringData() + } + #expect(throws: ORTError.self) { + _ = try string.tensorData() + } + } +}