Files
kangkang/康康/AI/ModelStore.swift
link2026 06484d09ff feat(AI): LLM 迁移到 mlx-swift-lm 2.31.3 + Qwen3.5-2B
将 SPM 依赖从 mlx-swift-examples 2.29.1 迁到改名延续仓库 mlx-swift-lm
2.31.3(含 qwen3_5 架构、旧 loadContainer API 兼容),文本 LLM 由
Qwen3-1.7B 换为 Qwen3.5-2B-4bit(走 qwen3_5→Qwen35Model 文本路径)。
连带 mlx-swift 0.29.1→0.31.4,顺修弃用 API:
- MLX.GPU.clearCache() → MLX.Memory.clearCache()
- MLX.GPU.set(cacheLimit:) → MLX.Memory.cacheLimit

更新 ModelManifest(.llm 文件清单+精确字节数,~1.63GiB)、ModelManifestTests、
HealthExport.modelTag 默认值。App BUILD SUCCEEDED + ModelManifestTests 通过。

保留作 MNN 改造的 GPU 兜底基线。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:00:28 +08:00

140 lines
5.5 KiB
Swift

import Foundation
nonisolated enum ModelKind: String, CaseIterable {
/// HuggingFace mlx-community , Models/
/// LLM Qwen3.5-2B(, mlx-swift-lm qwen3_5 Qwen35Model )
case llm = "Qwen3.5-2B-4bit"
case vl = "Qwen3-VL-4B-Instruct-4bit"
var displayName: String {
switch self {
case .llm: return "Qwen3.5-2B"
case .vl: return "Qwen3-VL-4B"
}
}
/// HuggingFace ID(org/name),
var huggingFaceRepo: String { "mlx-community/\(rawValue)" }
///
var sentinelFilename: String { "config.json" }
}
/// `@unchecked Sendable`:rootURL let, filesystem(线),
/// actor / Task 访
/// `nonisolated`: `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`,
/// MainActor, `AIRuntime` actor
final class ModelStore: @unchecked Sendable {
nonisolated static let shared: ModelStore = {
do {
let appSupport = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let root = appSupport.appendingPathComponent("Models", isDirectory: true)
return try ModelStore(rootURL: root)
} catch {
fatalError("ModelStore.shared init failed: \(error)")
}
}()
let rootURL: URL
init(rootURL: URL) throws {
self.rootURL = rootURL
try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
}
nonisolated func localURL(for kind: ModelKind) -> URL {
rootURL.appendingPathComponent(kind.rawValue, isDirectory: true)
}
nonisolated func isReady(_ kind: ModelKind) -> Bool {
let sentinel = localURL(for: kind).appendingPathComponent(kind.sentinelFilename)
return FileManager.default.fileExists(atPath: sentinel.path)
}
nonisolated func totalBytes(for kind: ModelKind) -> Int {
let folder = localURL(for: kind)
guard let enumerator = FileManager.default.enumerator(
at: folder,
includingPropertiesForKeys: [.fileSizeKey]
) else { return 0 }
var sum = 0
for case let url as URL in enumerator {
if let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize {
sum += size
}
}
return sum
}
/// Demo : Bundle (W6 使,)
nonisolated func seedFromBundle(_ kind: ModelKind) throws {
guard let bundleURL = Bundle.main.url(forResource: kind.rawValue, withExtension: nil) else {
#if DEBUG
assertionFailure("Bundle 缺少 \(kind.rawValue),检查资源是否加入 target")
#endif
return
}
let target = localURL(for: kind)
if FileManager.default.fileExists(atPath: target.path) {
try FileManager.default.removeItem(at: target)
}
try FileManager.default.copyItem(at: bundleURL, to: target)
}
// MARK: - /
/// URL
nonisolated func fileURL(for kind: ModelKind, relativePath: String) -> URL {
localURL(for: kind).appendingPathComponent(relativePath)
}
/// , 0()
nonisolated func localBytes(for kind: ModelKind, relativePath: String) -> Int {
let url = fileURL(for: kind, relativePath: relativePath)
guard let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize else { return 0 }
return size
}
/// :
/// `files` `ModelManifest`;
nonisolated func isComplete(for kind: ModelKind, files: [ModelFile]? = nil) -> Bool {
let manifest = files ?? ModelManifest.files(for: kind)
guard !manifest.isEmpty else { return false }
for file in manifest where localBytes(for: kind, relativePath: file.path) != file.bytes {
return false
}
return true
}
/// : config.json ()
nonisolated func importModel(_ kind: ModelKind, from sourceFolder: URL) throws {
let configPath = sourceFolder.appendingPathComponent(kind.sentinelFilename).path
guard FileManager.default.fileExists(atPath: configPath) else {
throw ModelStoreError.missingConfig
}
let target = localURL(for: kind)
if FileManager.default.fileExists(atPath: target.path) {
try FileManager.default.removeItem(at: target)
}
try FileManager.default.createDirectory(
at: target.deletingLastPathComponent(), withIntermediateDirectories: true)
try FileManager.default.copyItem(at: sourceFolder, to: target)
}
}
enum ModelStoreError: Error, LocalizedError {
case missingConfig
var errorDescription: String? {
switch self {
case .missingConfig:
return String(appLoc: "所选文件夹缺少 config.json,不是有效的模型目录")
}
}
}