fix(concurrency): clear 4 Swift 6 warnings under default MainActor isolation

- ModelStore/FileVault: drop nonisolated(unsafe) on shared, mark all instance
  methods nonisolated (they only read filesystem); ModelKind enum also nonisolated
- AIRuntime ↔ ModelStore cross-actor call resolved by the above
- LLMSession: replace deprecated Device.setDefault(device:) with task-scoped
  Device.withDefaultDevice(.cpu, body:); wrap both load and generate so the
  TaskLocal propagates through ModelContainer.perform
This commit is contained in:
link2026
2026-05-25 23:18:08 +08:00
parent 53da442424
commit e4a68a1bdd
3 changed files with 67 additions and 57 deletions

View File

@@ -12,18 +12,27 @@ actor LLMSession {
self.container = container self.container = container
} }
/// simulator CPU(MLX Metal backend Sim abort)
/// body (GPU/ANE)
/// task-scoped `withDefaultDevice`,TaskLocal child Task / actor
private static func withDeviceOverride<R>(
_ body: () async throws -> R
) async rethrows -> R {
#if targetEnvironment(simulator)
return try await Device.withDefaultDevice(.cpu, body)
#else
return try await body()
#endif
}
/// ( config.json + weights + tokenizer) /// ( config.json + weights + tokenizer)
static func load(folderURL: URL) async throws -> LLMSession { static func load(folderURL: URL) async throws -> LLMSession {
#if targetEnvironment(simulator)
// MLX iOS Simulator GPU stream Metal backend abort
// , CPU; GPU/ANE
Device.setDefault(device: .cpu)
#endif
let configuration = ModelConfiguration(directory: folderURL) let configuration = ModelConfiguration(directory: folderURL)
let container = try await LLMModelFactory.shared.loadContainer( let container = try await withDeviceOverride {
try await LLMModelFactory.shared.loadContainer(
configuration: configuration configuration: configuration
) )
}
return LLMSession(container: container) return LLMSession(container: container)
} }
@@ -35,6 +44,7 @@ actor LLMSession {
AsyncThrowingStream { continuation in AsyncThrowingStream { continuation in
let task = Task { let task = Task {
do { do {
try await Self.withDeviceOverride {
let parameters = GenerateParameters( let parameters = GenerateParameters(
maxTokens: maxTokens, maxTokens: maxTokens,
temperature: Float(0.6), temperature: Float(0.6),
@@ -76,6 +86,7 @@ actor LLMSession {
// ,GPU // ,GPU
// transitive import MLX , SPM // transitive import MLX , SPM
} }
}
continuation.finish() continuation.finish()
} catch { } catch {
continuation.finish(throwing: error) continuation.finish(throwing: error)

View File

@@ -1,6 +1,6 @@
import Foundation import Foundation
enum ModelKind: String, CaseIterable { nonisolated enum ModelKind: String, CaseIterable {
/// HuggingFace mlx-community , Models/ /// HuggingFace mlx-community , Models/
case llm = "Qwen3-1.7B-4bit" case llm = "Qwen3-1.7B-4bit"
case vl = "Qwen2.5-VL-3B-Instruct-4bit" case vl = "Qwen2.5-VL-3B-Instruct-4bit"
@@ -21,10 +21,10 @@ enum ModelKind: String, CaseIterable {
/// `@unchecked Sendable`:rootURL let, filesystem(线), /// `@unchecked Sendable`:rootURL let, filesystem(线),
/// actor / Task 访 /// actor / Task 访
/// `nonisolated(unsafe) shared`: `-default-isolation=MainActor` /// `nonisolated`: `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`,
/// static MainActor , actor 访 await opt-out /// MainActor, `AIRuntime` actor
final class ModelStore: @unchecked Sendable { final class ModelStore: @unchecked Sendable {
nonisolated(unsafe) static let shared: ModelStore = { nonisolated static let shared: ModelStore = {
do { do {
let appSupport = try FileManager.default.url( let appSupport = try FileManager.default.url(
for: .applicationSupportDirectory, for: .applicationSupportDirectory,
@@ -46,16 +46,16 @@ final class ModelStore: @unchecked Sendable {
try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
} }
func localURL(for kind: ModelKind) -> URL { nonisolated func localURL(for kind: ModelKind) -> URL {
rootURL.appendingPathComponent(kind.rawValue, isDirectory: true) rootURL.appendingPathComponent(kind.rawValue, isDirectory: true)
} }
func isReady(_ kind: ModelKind) -> Bool { nonisolated func isReady(_ kind: ModelKind) -> Bool {
let sentinel = localURL(for: kind).appendingPathComponent(kind.sentinelFilename) let sentinel = localURL(for: kind).appendingPathComponent(kind.sentinelFilename)
return FileManager.default.fileExists(atPath: sentinel.path) return FileManager.default.fileExists(atPath: sentinel.path)
} }
func totalBytes(for kind: ModelKind) -> Int { nonisolated func totalBytes(for kind: ModelKind) -> Int {
let folder = localURL(for: kind) let folder = localURL(for: kind)
guard let enumerator = FileManager.default.enumerator( guard let enumerator = FileManager.default.enumerator(
at: folder, at: folder,
@@ -71,7 +71,7 @@ final class ModelStore: @unchecked Sendable {
} }
/// Demo : Bundle (W6 使,) /// Demo : Bundle (W6 使,)
func seedFromBundle(_ kind: ModelKind) throws { nonisolated func seedFromBundle(_ kind: ModelKind) throws {
guard let bundleURL = Bundle.main.url(forResource: kind.rawValue, withExtension: nil) else { guard let bundleURL = Bundle.main.url(forResource: kind.rawValue, withExtension: nil) else {
#if DEBUG #if DEBUG
assertionFailure("Bundle 缺少 \(kind.rawValue),检查资源是否加入 target") assertionFailure("Bundle 缺少 \(kind.rawValue),检查资源是否加入 target")

View File

@@ -9,10 +9,9 @@ enum FileVaultError: Error {
} }
/// `@unchecked Sendable`:rootURL let, I/O (线), /// `@unchecked Sendable`:rootURL let, I/O (线),
/// actor / Task 访 /// actor / Task 访 `nonisolated`, ModelStore
/// `nonisolated(unsafe) shared`: ModelStore
final class FileVault: @unchecked Sendable { final class FileVault: @unchecked Sendable {
nonisolated(unsafe) static let shared: FileVault = { nonisolated static let shared: FileVault = {
do { do {
let appSupport = try FileManager.default.url( let appSupport = try FileManager.default.url(
for: .applicationSupportDirectory, for: .applicationSupportDirectory,
@@ -45,7 +44,7 @@ final class FileVault: @unchecked Sendable {
// MARK: - Path Safety // MARK: - Path Safety
private func resolveSafePath(_ relativePath: String) throws -> URL { nonisolated private func resolveSafePath(_ relativePath: String) throws -> URL {
guard !relativePath.contains("/"), guard !relativePath.contains("/"),
!relativePath.contains(".."), !relativePath.contains(".."),
!relativePath.isEmpty else { !relativePath.isEmpty else {
@@ -60,7 +59,7 @@ final class FileVault: @unchecked Sendable {
// MARK: - Public API // MARK: - Public API
func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> SavedAsset { nonisolated func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> SavedAsset {
guard let data = image.jpegData(compressionQuality: quality) else { guard let data = image.jpegData(compressionQuality: quality) else {
throw FileVaultError.writeFailed throw FileVaultError.writeFailed
} }
@@ -70,7 +69,7 @@ final class FileVault: @unchecked Sendable {
return SavedAsset(relativePath: filename, bytes: data.count) return SavedAsset(relativePath: filename, bytes: data.count)
} }
func loadImage(relativePath: String) throws -> UIImage { nonisolated func loadImage(relativePath: String) throws -> UIImage {
let url = try resolveSafePath(relativePath) let url = try resolveSafePath(relativePath)
let data: Data let data: Data
do { do {
@@ -82,7 +81,7 @@ final class FileVault: @unchecked Sendable {
return image return image
} }
func remove(relativePath: String) throws { nonisolated func remove(relativePath: String) throws {
let url = try resolveSafePath(relativePath) let url = try resolveSafePath(relativePath)
do { do {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
@@ -91,7 +90,7 @@ final class FileVault: @unchecked Sendable {
} }
} }
func wipe() throws { nonisolated func wipe() throws {
let fm = FileManager.default let fm = FileManager.default
let contents = try fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil) let contents = try fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil)
for url in contents { for url in contents {