Files
wehub-resource-sync e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:59:26 +08:00

339 lines
15 KiB
Swift

// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// Preset for which ONNX Runtime execution provider is registered first on the session chain in this demo.
enum ORTPrimaryExecutionProvider: String, CaseIterable, Identifiable, Sendable {
/// Default CPU execution. Default for this demo.
case cpu = "cpu"
/// Registers the XNNPACK EP; nodes it cannot run still use the CPU EP.
case xnnpack = "xnnpack"
/// Registers the Core ML EP; remaining subgraphs use other registered EPs (commonly CPU).
case coreML = "core_ml"
var id: String { rawValue }
var displayTitle: String {
switch self {
case .cpu: return "CPU"
case .xnnpack: return "XNNPACK"
case .coreML: return "Core ML"
}
}
}
enum ORTCoreMLProviderOption: String, CaseIterable, Identifiable, Sendable {
case enableOnSubgraphs
case createMLProgram
case staticInputShapes
case cpuOnly
case cpuAndGPU
case aneOnly
var id: String { rawValue }
var displayTitle: String {
switch self {
case .enableOnSubgraphs: return "Enable on subgraphs"
case .createMLProgram: return "Use ML program"
case .staticInputShapes: return "Fixed tensor shapes only"
case .cpuOnly: return "Core ML CPU only"
case .cpuAndGPU: return "Core ML CPU and GPU"
case .aneOnly: return "Only on devices with ANE"
}
}
var detailCaption: String {
switch self {
case .enableOnSubgraphs: return "Compile subgraphs independently for Core ML."
case .createMLProgram: return "Prefer MLProgram over NeuralNetwork (when supported)."
case .staticInputShapes:
return "Core ML tries to run only ops whose input tensors have fixed shapes at inference time; parts with varying dimensions typically stay on the CPU EP instead."
case .cpuOnly: return "Forces Core ML compute on CPU. Mutually exclusive with CPU and GPU."
case .cpuAndGPU: return "Allows Core ML to use CPU and GPU. Mutually exclusive with CPU only."
case .aneOnly: return "Only enable Core ML EP on devices that have Apple Neural Engine hardware."
}
}
}
enum ORTSessionManagerError: LocalizedError {
case modelNotFound(String)
case sessionCreationFailed(String)
case inferenceFailed(String)
case inputTensorElementCountMismatch(expected: Int, actual: Int, modelName: String)
case outputContainsNaN(String)
case ortProfilingFailed(String)
var errorDescription: String? {
switch self {
case .modelNotFound(let name): return "Model not found: \(name)"
case .sessionCreationFailed(let detail): return "Session creation failed: \(detail)"
case .inferenceFailed(let detail): return "Inference failed: \(detail)"
case .inputTensorElementCountMismatch(let expected, let actual, let modelName):
return "\(modelName): input tensor size mismatch (expected \(expected) floats, got \(actual))"
case .outputContainsNaN(let name): return "Output tensor '\(name)' contains NaN values"
case .ortProfilingFailed(let detail): return "ONNX Runtime profiling: \(detail)"
}
}
}
/// File URLs of ONNX Runtime JSON profiles after ``ORTSessionManager/finalizeORTProfiling()`` (separate file per sub-model).
struct ORTProfilingOutput: Sendable {
var detectionProfileJSON: URL
var recognitionProfileJSON: URL
}
/// Tuning options for ORT session creation.
struct ORTSessionTuningOptions: Sendable {
/// Session-level intra-op thread count. 0 = ORT default.
var intraOpThreads: Int = 0
/// XNNPACK EP thread count. 0 = ORT default.
var xnnpackThreads: Int = 0
/// CoreML EP flags: enableOnSubgraphs, createMLProgram, staticInputShapes, cpuOnly, cpuAndGPU, aneOnly.
var coreMLFlags: Set<String> = []
static let `default` = ORTSessionTuningOptions()
}
extension ORTSessionTuningOptions: Equatable {}
/// At most one placement-related Core ML EP option is applied. Priority matches
/// ``OnnxRuntimeSettingsPanel`` when several keys appear (e.g. benchmark env / JSON).
private func applyCoreMLPlacementFlags(from flags: Set<String>, to coreml: ORTCoreMLExecutionProviderOptions) {
if flags.contains(ORTCoreMLProviderOption.cpuOnly.rawValue) {
coreml.useCPUOnly = true
} else if flags.contains(ORTCoreMLProviderOption.cpuAndGPU.rawValue) {
coreml.useCPUAndGPU = true
} else if flags.contains(ORTCoreMLProviderOption.aneOnly.rawValue) {
coreml.onlyEnableForDevicesWithANE = true
}
}
actor ORTSessionManager {
private struct SessionIONames {
let inputName: String
let outputNames: Set<String>
}
private var env: ORTEnv?
private var detSession: ORTSession?
private var recSession: ORTSession?
private var detIO: SessionIONames?
private var recIO: SessionIONames?
private var ortProfilingPendingFinalize: Bool = false
/// Load both models. Creates one ORTEnv (per ORT docs: one per process),
/// configures execution providers per ``ORTPrimaryExecutionProvider``,
/// and creates sessions for detection and recognition models.
///
/// - Parameter ortProfiling: When `true`, enables ONNX Runtime session profiling (written as JSON on finalize).
/// Profiling adds overhead; do not treat wall-clock times from the same run as a clean latency benchmark.
/// - Parameter tuning: Session tuning options (threads, EP flags). Default reads from environment.
func loadModels(executionProvider: ORTPrimaryExecutionProvider = .cpu, ortProfiling: Bool = false, tuning: ORTSessionTuningOptions = .default) async throws {
let env = try ORTEnv(loggingLevel: .warning)
self.env = env
ortProfilingPendingFinalize = ortProfiling
let detConfig = try ModelConfig.detection()
let recConfig = try ModelConfig.recognition()
if ortProfiling {
let baseDir = try ortProfilingDirectoryURL()
let detPrefix = baseDir.appendingPathComponent("paddle_ort_det", isDirectory: false).path
let recPrefix = baseDir.appendingPathComponent("paddle_ort_rec", isDirectory: false).path
let detOptions = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: detPrefix, tuning: tuning)
let recOptions = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: recPrefix, tuning: tuning)
detSession = try ORTSession(env: env, modelPath: detConfig.modelPath, sessionOptions: detOptions)
recSession = try ORTSession(env: env, modelPath: recConfig.modelPath, sessionOptions: recOptions)
} else {
let options = try makeSessionOptions(executionProvider: executionProvider, ortProfilePathPrefix: nil, tuning: tuning)
detSession = try ORTSession(env: env, modelPath: detConfig.modelPath, sessionOptions: options)
recSession = try ORTSession(env: env, modelPath: recConfig.modelPath, sessionOptions: options)
}
if let detSession {
detIO = try makeSessionIONames(session: detSession, modelName: "det")
}
if let recSession {
recIO = try makeSessionIONames(session: recSession, modelName: "rec")
}
}
/// Flushes ORT profiling and returns the JSON file paths. Call once after the last inference when
/// ``loadModels(ortProfiling:)`` was `true`. Safe to call when profiling was disabled (returns `nil`).
func finalizeORTProfiling() throws -> ORTProfilingOutput? {
guard ortProfilingPendingFinalize else { return nil }
ortProfilingPendingFinalize = false
guard let det = detSession, let rec = recSession else {
throw ORTSessionManagerError.sessionCreationFailed("Cannot finalize ORT profiling: session missing")
}
let detPath = try ORTProfilingBridge.endProfiling(session: det)
let recPath = try ORTProfilingBridge.endProfiling(session: rec)
if detPath.isEmpty || recPath.isEmpty {
throw ORTSessionManagerError.ortProfilingFailed("EndProfiling returned an empty path")
}
return ORTProfilingOutput(
detectionProfileJSON: URL(fileURLWithPath: detPath),
recognitionProfileJSON: URL(fileURLWithPath: recPath)
)
}
private func ortProfilingDirectoryURL() throws -> URL {
let base = try FileManager.default.url(
for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let dir = base.appendingPathComponent("PaddleOCRORTProfiling", isDirectory: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
private func makeSessionOptions(executionProvider: ORTPrimaryExecutionProvider, ortProfilePathPrefix: String?, tuning: ORTSessionTuningOptions) throws -> ORTSessionOptions {
let options = try ORTSessionOptions()
try options.setGraphOptimizationLevel(.all)
if tuning.intraOpThreads > 0 {
try options.setIntraOpNumThreads(Int32(tuning.intraOpThreads))
}
if let prefix = ortProfilePathPrefix {
try ORTProfilingBridge.enableProfiling(sessionOptions: options, pathPrefix: prefix)
}
switch executionProvider {
case .cpu:
break
case .xnnpack:
let xnnpackOptions = ORTXnnpackExecutionProviderOptions()
if tuning.xnnpackThreads > 0 {
xnnpackOptions.intra_op_num_threads = Int32(tuning.xnnpackThreads)
}
try options.appendXnnpackExecutionProvider(with: xnnpackOptions)
case .coreML:
let coremlOptions = ORTCoreMLExecutionProviderOptions()
let flags = tuning.coreMLFlags
if flags.contains("enableOnSubgraphs") { coremlOptions.enableOnSubgraphs = true }
if flags.contains("createMLProgram") { coremlOptions.createMLProgram = true }
if flags.contains("staticInputShapes") { coremlOptions.onlyAllowStaticInputShapes = true }
applyCoreMLPlacementFlags(from: flags, to: coremlOptions)
try options.appendCoreMLExecutionProvider(with: coremlOptions)
}
return options
}
private func makeSessionIONames(session: ORTSession, modelName: String) throws -> SessionIONames {
let inputNames = try session.inputNames()
let outputNamesList = try session.outputNames()
guard let firstInputName = inputNames.first else {
throw ORTSessionManagerError.inferenceFailed("\(modelName): no input names found")
}
return SessionIONames(inputName: firstInputName, outputNames: Set(outputNamesList))
}
/// Run detection inference with real preprocessed input data.
///
/// - Parameters:
/// - inputData: Float32 array in CHW layout; length must equal the product of `shape`.
/// - shape: Tensor shape, e.g. [1, 3, 896, 960].
/// - Returns: Dictionary mapping output name to (data as [Float], shape as [Int]).
func runDetection(inputData: [Float], shape: [Int]) async throws -> [String: (data: [Float], shape: [Int])] {
guard let session = detSession else {
throw ORTSessionManagerError.sessionCreationFailed("Detection session not loaded")
}
guard let io = detIO else {
throw ORTSessionManagerError.sessionCreationFailed("Detection session IO names not loaded")
}
return try runInference(session: session, io: io, modelName: "det", inputData: inputData, shape: shape)
}
/// Run recognition inference with preprocessed input data.
///
/// The recognition model accepts dynamic batch and width: shape `[N, 3, H, W]` with `N ≥ 1`,
/// fixed `H` (e.g. 48), and `W` shared after padding within each batch. A single line uses `N = 1`.
///
/// - Parameters:
/// - inputData: Float32 row-major tensor; length must equal the product of `shape`.
/// - shape: e.g. `[1, 3, 48, 320]` or `[6, 3, 48, 640]` for a batch of six lines.
/// - Returns: Dictionary mapping output name to (data as [Float], shape as [Int]).
func runRecognition(inputData: [Float], shape: [Int]) async throws -> [String: (data: [Float], shape: [Int])] {
guard let session = recSession else {
throw ORTSessionManagerError.sessionCreationFailed("Recognition session not loaded")
}
guard let io = recIO else {
throw ORTSessionManagerError.sessionCreationFailed("Recognition session IO names not loaded")
}
return try runInference(session: session, io: io, modelName: "rec", inputData: inputData, shape: shape)
}
/// Shared inference logic used by both detection and recognition.
///
/// Discovers input/output names at runtime, creates a float32 tensor from the
/// provided data and shape, runs the session, extracts output arrays, and
/// validates that no output contains NaN values.
private func runInference(
session: ORTSession,
io: SessionIONames,
modelName: String,
inputData: [Float],
shape: [Int]
) throws -> [String: (data: [Float], shape: [Int])] {
let expectedElements = shape.reduce(1, *)
guard expectedElements == inputData.count else {
throw ORTSessionManagerError.inputTensorElementCountMismatch(
expected: expectedElements,
actual: inputData.count,
modelName: modelName
)
}
// Create ORT tensor from input data
let nsShape = shape.map { NSNumber(value: $0) }
var data = inputData
let tensorData = NSMutableData(
bytes: &data,
length: inputData.count * MemoryLayout<Float>.stride
)
let inputTensor = try ORTValue(
tensorData: tensorData,
elementType: .float,
shape: nsShape
)
let inputs: [String: ORTValue] = [io.inputName: inputTensor]
let outputs = try session.run(
withInputs: inputs,
outputNames: io.outputNames,
runOptions: nil
)
// Extract output tensors as Float arrays
var result: [String: (data: [Float], shape: [Int])] = [:]
for (name, value) in outputs {
let info = try value.tensorTypeAndShapeInfo()
let outputShape = info.shape.map { $0.intValue }
let outputData = try value.tensorData() as Data
let floats: [Float] = outputData.withUnsafeBytes { buffer in
Array(buffer.bindMemory(to: Float.self))
}
if floats.contains(where: \.isNaN) {
throw ORTSessionManagerError.outputContainsNaN(name)
}
result[name] = (data: floats, shape: outputShape)
}
return result
}
}