From c201da3a5b9e01c230a9cc2768ff91d53ede02cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aykut=20Gu=CC=88ven?= Date: Tue, 24 Feb 2026 20:38:29 +0100 Subject: [PATCH 1/4] Mark CoreML and AVFoundation @preconcurrency --- Sources/WhisperKit/Core/Audio/AudioProcessor.swift | 2 +- Sources/WhisperKit/Utilities/Extensions+Public.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/WhisperKit/Core/Audio/AudioProcessor.swift b/Sources/WhisperKit/Core/Audio/AudioProcessor.swift index e19ea453..14e5b68e 100644 --- a/Sources/WhisperKit/Core/Audio/AudioProcessor.swift +++ b/Sources/WhisperKit/Core/Audio/AudioProcessor.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Argmax, Inc. All rights reserved. import Accelerate -import AVFoundation +@preconcurrency import AVFoundation import CoreAudio import CoreML diff --git a/Sources/WhisperKit/Utilities/Extensions+Public.swift b/Sources/WhisperKit/Utilities/Extensions+Public.swift index ae04c798..71d789aa 100644 --- a/Sources/WhisperKit/Utilities/Extensions+Public.swift +++ b/Sources/WhisperKit/Utilities/Extensions+Public.swift @@ -2,7 +2,8 @@ // Copyright © 2024 Argmax, Inc. All rights reserved. import AVFoundation -import CoreML +// TODO: Should be able to remove `@preconcurrency` once we drop support for iOS 16, macOS 14. +@preconcurrency import CoreML public extension Array where Element == TranscriptionSegment { func contains(segment: TranscriptionSegment) -> Bool { From 6512a3908e576a3222bb6b3929bfcb4d2e40fe4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aykut=20Gu=CC=88ven?= Date: Tue, 24 Feb 2026 20:50:42 +0100 Subject: [PATCH 2/4] Fix concurrency errors in MLTensor extension --- .../WhisperKit/Core/Text/TokenSampler.swift | 16 ++-- Sources/WhisperKit/Core/TextDecoder.swift | 4 +- .../Utilities/Extensions+Public.swift | 83 ++++++------------- .../MLTensorExtensionsTests.swift | 74 +++++++++++++++++ 4 files changed, 111 insertions(+), 66 deletions(-) create mode 100644 Tests/WhisperKitTests/MLTensorExtensionsTests.swift diff --git a/Sources/WhisperKit/Core/Text/TokenSampler.swift b/Sources/WhisperKit/Core/Text/TokenSampler.swift index 8d710518..57b08eda 100644 --- a/Sources/WhisperKit/Core/Text/TokenSampler.swift +++ b/Sources/WhisperKit/Core/Text/TokenSampler.swift @@ -6,7 +6,7 @@ import CoreML import Foundation public protocol TokenSampling { - func update(tokens: [Int], logits: MLMultiArray, logProbs: [Float]) -> SamplingResult + func update(tokens: [Int], logits: MLMultiArray, logProbs: [Float]) async -> SamplingResult func finalize(tokens: [Int], logProbs: [Float]) -> SamplingResult } @@ -39,7 +39,7 @@ open class GreedyTokenSampler: TokenSampling { #if canImport(CoreML.MLState) @available(macOS 15, iOS 18, watchOS 11, visionOS 2, *) - private func sampleWithMLTensor(logits: MLMultiArray) -> (token: Int, logprob: Float) { + private func sampleWithMLTensor(logits: MLMultiArray) async -> (token: Int, logprob: Float) { // Use MLTensor operations if available for sampling // Reference: https://github.com/huggingface/swift-transformers/blob/preview/Sources/Generation/Decoders.swift var logitsTensor = MLTensor(MLShapedArray(logits)).cast(to: Float.self) @@ -76,9 +76,11 @@ open class GreedyTokenSampler: TokenSampling { nextLogprobTensor = softmaxScores.gathering(atIndices: nextTokenTensor, alongAxis: -1).log() } + async let nextTokenArray = nextTokenTensor.asIntArray() + async let nextLogprobArray = nextLogprobTensor.asFloatArray() return ( - token: nextTokenTensor.asIntArray()[0], - logprob: nextLogprobTensor.asFloatArray()[0] + token: await nextTokenArray[0], + logprob: await nextLogprobArray[0] ) } #endif @@ -212,7 +214,7 @@ open class GreedyTokenSampler: TokenSampling { return (token: nextToken!, logprob: nextLogprob) } - public func update(tokens: [Int], logits: MLMultiArray, logProbs: [Float]) -> SamplingResult { + public func update(tokens: [Int], logits: MLMultiArray, logProbs: [Float]) async -> SamplingResult { var nextTokens = tokens var nextLogprobs = logProbs var completed = false @@ -220,7 +222,7 @@ open class GreedyTokenSampler: TokenSampling { var result: (token: Int, logprob: Float) #if canImport(CoreML.MLState) if #available(macOS 15.0, iOS 18.0, watchOS 11.0, visionOS 2.0, *) { - result = sampleWithMLTensor(logits: logits) + result = await sampleWithMLTensor(logits: logits) } else { result = sampleWithBNNS(logits: logits) } @@ -278,7 +280,7 @@ open class BeamSearchTokenSampler: TokenSampling { finishedSequences = [] } - public func update(tokens: [Int], logits: MLMultiArray, logProbs: [Float]) -> SamplingResult { + public func update(tokens: [Int], logits: MLMultiArray, logProbs: [Float]) async -> SamplingResult { // TODO: Implement fatalError("Not implemented: \(#function)") } diff --git a/Sources/WhisperKit/Core/TextDecoder.swift b/Sources/WhisperKit/Core/TextDecoder.swift index efdacecc..6fcedcf3 100644 --- a/Sources/WhisperKit/Core/TextDecoder.swift +++ b/Sources/WhisperKit/Core/TextDecoder.swift @@ -686,7 +686,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { let samplingStartTime = Date() - let sampleResult = tokenSampler.update(tokens: currentTokens, logits: logits, logProbs: logProbs) + let sampleResult = await tokenSampler.update(tokens: currentTokens, logits: logits, logProbs: logProbs) nextToken = sampleResult.tokens.last! logProbs = sampleResult.logProbs @@ -838,7 +838,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { let samplingStartTime = Date() - let sampleResult = tokenSampler.update(tokens: currentTokens, logits: logits, logProbs: logProbs) + let sampleResult = await tokenSampler.update(tokens: currentTokens, logits: logits, logProbs: logProbs) nextToken = sampleResult.tokens.last! let nextTokenLogProb = sampleResult.logProbs.last! diff --git a/Sources/WhisperKit/Utilities/Extensions+Public.swift b/Sources/WhisperKit/Utilities/Extensions+Public.swift index 71d789aa..60472b8d 100644 --- a/Sources/WhisperKit/Utilities/Extensions+Public.swift +++ b/Sources/WhisperKit/Utilities/Extensions+Public.swift @@ -160,69 +160,38 @@ public extension MLMultiArray { #if canImport(CoreML.MLState) @available(macOS 15.0, iOS 18.0, watchOS 11.0, visionOS 2.0, *) public extension MLTensor { - func asIntArray() -> [Int] { - let semaphore = DispatchSemaphore(value: 0) - var result: [Int] = [] - - Task(priority: .high) { - result = await self.shapedArray(of: Int32.self).scalars.map { Int($0) } - semaphore.signal() - } - - semaphore.wait() - return result + func asIntArray() async -> [Int] { + await shapedArray(of: Int32.self).scalars.map { Int($0) } } - func asFloatArray() -> [Float] { - let semaphore = DispatchSemaphore(value: 0) - let tensorType = self.scalarType - - var result: [Float] = [] - - Task(priority: .high) { - switch tensorType { - case is Float32.Type: - result = await self.shapedArray(of: Float32.self).scalars.map { Float($0) } - case is FloatType.Type: - result = await self.shapedArray(of: FloatType.self).scalars.map { Float($0) } - case is Float.Type: - result = await self.shapedArray(of: Float.self).scalars.map { Float($0) } - case is Int32.Type: - result = await self.shapedArray(of: Int32.self).scalars.map { Float($0) } - default: - fatalError("Unsupported data type") - } - semaphore.signal() + func asFloatArray() async -> [Float] { + switch scalarType { + case is Float32.Type: + await shapedArray(of: Float32.self).scalars.map { Float($0) } + case is FloatType.Type: + await shapedArray(of: FloatType.self).scalars.map { Float($0) } + case is Float.Type: + await shapedArray(of: Float.self).scalars.map { Float($0) } + case is Int32.Type: + await shapedArray(of: Int32.self).scalars.map { Float($0) } + default: + fatalError("Unsupported data type") } - - semaphore.wait() - return result } - func asMLMultiArray() -> MLMultiArray { - let semaphore = DispatchSemaphore(value: 0) - let tensorType = self.scalarType - - var result = try! MLMultiArray(shape: [1], dataType: .float16, initialValue: 0.0) - - Task(priority: .high) { - switch tensorType { - case is Float32.Type: - result = MLMultiArray(await self.shapedArray(of: Float32.self)) - case is FloatType.Type: - result = MLMultiArray(await self.shapedArray(of: FloatType.self)) - case is Float.Type: - result = MLMultiArray(await self.shapedArray(of: Float.self)) - case is Int32.Type: - result = MLMultiArray(await self.shapedArray(of: Int32.self)) - default: - fatalError("Unsupported data type") - } - semaphore.signal() + func asMLMultiArray() async -> MLMultiArray { + switch scalarType { + case is Float32.Type: + MLMultiArray(await shapedArray(of: Float32.self)) + case is FloatType.Type: + MLMultiArray(await shapedArray(of: FloatType.self)) + case is Float.Type: + MLMultiArray(await shapedArray(of: Float.self)) + case is Int32.Type: + MLMultiArray(await shapedArray(of: Int32.self)) + default: + fatalError("Unsupported data type") } - - semaphore.wait() - return result } } #endif diff --git a/Tests/WhisperKitTests/MLTensorExtensionsTests.swift b/Tests/WhisperKitTests/MLTensorExtensionsTests.swift new file mode 100644 index 00000000..d8c74071 --- /dev/null +++ b/Tests/WhisperKitTests/MLTensorExtensionsTests.swift @@ -0,0 +1,74 @@ +// For licensing see accompanying LICENSE.md file. +// Copyright © 2024 Argmax, Inc. All rights reserved. + +#if canImport(CoreML.MLState) +import CoreML +@testable import WhisperKit +import XCTest + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, visionOS 2.0, *) +final class MLTensorExtensionsTests: XCTestCase { + func testAsIntArrayReturnsExpectedScalars() async { + let tensor = MLTensor(MLShapedArray(scalars: [1, -2, 42], shape: [3])) + + let result = await tensor.asIntArray() + + XCTAssertEqual(result, [1, -2, 42]) + } + + func testAsFloatArraySupportsFloat32Tensor() async { + let tensor = MLTensor(MLShapedArray(scalars: [0.25, -1.5, 2.0], shape: [3])) + + let result = await tensor.asFloatArray() + + assertEqual(result, [0.25, -1.5, 2.0], accuracy: 0.0001) + } + + func testAsFloatArraySupportsFloatTypeTensor() async { + let expected = [FloatType(0.125), FloatType(-0.75), FloatType(3.5)] + let tensor = MLTensor(MLShapedArray(scalars: expected, shape: [3])) + + let result = await tensor.asFloatArray() + + assertEqual(result, expected.map(Float.init), accuracy: 0.0001) + } + + func testAsFloatArraySupportsInt32Tensor() async { + let tensor = MLTensor(MLShapedArray(scalars: [-3, 0, 7], shape: [3])) + + let result = await tensor.asFloatArray() + + assertEqual(result, [-3, 0, 7], accuracy: 0.0001) + } + + func testAsMLMultiArrayRoundTripsFloatTypeTensor() async { + let expected = [FloatType(1.25), FloatType(-0.5), FloatType(3.75)] + let tensor = MLTensor(MLShapedArray(scalars: expected, shape: [3])) + + let result = await tensor.asMLMultiArray() + let shapedArray = MLShapedArray(result) + + XCTAssertEqual(result.shape, [3]) + XCTAssertEqual(shapedArray.scalars.count, expected.count) + assertEqual(shapedArray.scalars.map(Float.init), expected.map(Float.init), accuracy: 0.0001) + } + + func testAsMLMultiArrayRoundTripsInt32Tensor() async { + let expected: [Int32] = [-9, 4, 12] + let tensor = MLTensor(MLShapedArray(scalars: expected, shape: [3])) + + let result = await tensor.asMLMultiArray() + let shapedArray = MLShapedArray(result) + + XCTAssertEqual(result.shape, [3]) + XCTAssertEqual(shapedArray.scalars, expected) + } + + private func assertEqual(_ lhs: [Float], _ rhs: [Float], accuracy: Float) { + XCTAssertEqual(lhs.count, rhs.count) + for (actual, expected) in zip(lhs, rhs) { + XCTAssertEqual(actual, expected, accuracy: accuracy) + } + } +} +#endif From 16cd25229db2add91fd9a4aa304e9c4a4b568c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aykut=20G=C3=BCven?= Date: Tue, 24 Feb 2026 22:52:09 +0100 Subject: [PATCH 3/4] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Sources/WhisperKit/Utilities/Extensions+Public.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WhisperKit/Utilities/Extensions+Public.swift b/Sources/WhisperKit/Utilities/Extensions+Public.swift index 60472b8d..e636ff6b 100644 --- a/Sources/WhisperKit/Utilities/Extensions+Public.swift +++ b/Sources/WhisperKit/Utilities/Extensions+Public.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Argmax, Inc. All rights reserved. import AVFoundation -// TODO: Should be able to remove `@preconcurrency` once we drop support for iOS 16, macOS 14. +// TODO: Should be able to remove `@preconcurrency` once we drop support for iOS 16 and macOS 13. @preconcurrency import CoreML public extension Array where Element == TranscriptionSegment { From 16946dc121b2b619c8608f34b29c7170001aecca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aykut=20Gu=CC=88ven?= Date: Wed, 25 Feb 2026 09:58:46 +0100 Subject: [PATCH 4/4] Fix type inference issue --- Tests/WhisperKitTests/MLTensorExtensionsTests.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/WhisperKitTests/MLTensorExtensionsTests.swift b/Tests/WhisperKitTests/MLTensorExtensionsTests.swift index d8c74071..39599af0 100644 --- a/Tests/WhisperKitTests/MLTensorExtensionsTests.swift +++ b/Tests/WhisperKitTests/MLTensorExtensionsTests.swift @@ -29,8 +29,9 @@ final class MLTensorExtensionsTests: XCTestCase { let tensor = MLTensor(MLShapedArray(scalars: expected, shape: [3])) let result = await tensor.asFloatArray() + let expectedFloats: [Float] = expected.map { Float($0) } - assertEqual(result, expected.map(Float.init), accuracy: 0.0001) + assertEqual(result, expectedFloats, accuracy: 0.0001) } func testAsFloatArraySupportsInt32Tensor() async { @@ -47,10 +48,12 @@ final class MLTensorExtensionsTests: XCTestCase { let result = await tensor.asMLMultiArray() let shapedArray = MLShapedArray(result) + let resultFloats: [Float] = shapedArray.scalars.map { Float($0) } + let expectedFloats: [Float] = expected.map { Float($0) } XCTAssertEqual(result.shape, [3]) XCTAssertEqual(shapedArray.scalars.count, expected.count) - assertEqual(shapedArray.scalars.map(Float.init), expected.map(Float.init), accuracy: 0.0001) + assertEqual(resultFloats, expectedFloats, accuracy: 0.0001) } func testAsMLMultiArrayRoundTripsInt32Tensor() async {