```
feat(debug): 添加模型导入功能并修复模拟器GPU初始化问题 - 在DebugAIRunner中添加文件导入器,支持用户选择并导入LLM模型文件夹 - 添加导入状态管理和错误提示功能 - 修复iOS模拟器环境下MLX GPU stream初始化崩溃问题,强制使用CPU模式 - 添加UniformTypeIdentifiers导入以支持文件选择功能 ```
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import MLX
|
||||||
import MLXLLM
|
import MLXLLM
|
||||||
import MLXLMCommon
|
import MLXLMCommon
|
||||||
|
|
||||||
@@ -13,6 +14,12 @@ actor LLMSession {
|
|||||||
|
|
||||||
/// 从本地目录加载模型(包含 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 LLMModelFactory.shared.loadContainer(
|
||||||
configuration: configuration
|
configuration: configuration
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
/// DEBUG 自检:加载 LLM 并跑一段 prompt,流式显示 token + 速率。
|
/// DEBUG 自检:加载 LLM 并跑一段 prompt,流式显示 token + 速率。
|
||||||
/// 同时显示沙盒 Application Support 路径,方便把模型拷进去。
|
/// 同时显示沙盒 Application Support 路径,方便把模型拷进去。
|
||||||
@@ -10,6 +11,8 @@ struct DebugAIRunner: View {
|
|||||||
@State private var rate: Double = 0
|
@State private var rate: Double = 0
|
||||||
@State private var running = false
|
@State private var running = false
|
||||||
@State private var modelReady: Bool = false
|
@State private var modelReady: Bool = false
|
||||||
|
@State private var importingModel = false
|
||||||
|
@State private var importError: String?
|
||||||
|
|
||||||
private var appSupportPath: String {
|
private var appSupportPath: String {
|
||||||
(try? FileManager.default.url(
|
(try? FileManager.default.url(
|
||||||
@@ -47,12 +50,25 @@ struct DebugAIRunner: View {
|
|||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
|
|
||||||
|
Button(importingModel ? "导入中..." : "导入模型") {
|
||||||
|
importingModel = true
|
||||||
|
}
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.disabled(importingModel)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(modelReady ? "✓ 模型就绪" : "⚠ 模型未就绪")
|
Text(modelReady ? "✓ 模型就绪" : "⚠ 模型未就绪")
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundStyle(modelReady ? Tj.Palette.leaf : Tj.Palette.brick)
|
.foregroundStyle(modelReady ? Tj.Palette.leaf : Tj.Palette.brick)
|
||||||
}
|
}
|
||||||
|
if let importError {
|
||||||
|
Text(importError)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundStyle(Tj.Palette.brick)
|
||||||
|
.lineLimit(3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.background(
|
.background(
|
||||||
@@ -95,12 +111,69 @@ struct DebugAIRunner: View {
|
|||||||
)
|
)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.onAppear { refreshModelStatus() }
|
.onAppear { refreshModelStatus() }
|
||||||
|
.fileImporter(
|
||||||
|
isPresented: $importingModel,
|
||||||
|
allowedContentTypes: [.folder],
|
||||||
|
allowsMultipleSelection: false
|
||||||
|
) { result in
|
||||||
|
importModelFolder(from: result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshModelStatus() {
|
private func refreshModelStatus() {
|
||||||
modelReady = ModelStore.shared.isReady(.llm)
|
modelReady = ModelStore.shared.isReady(.llm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func importModelFolder(from result: Result<[URL], Error>) {
|
||||||
|
defer {
|
||||||
|
refreshModelStatus()
|
||||||
|
importingModel = false
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard let pickedURL = try result.get().first else {
|
||||||
|
importError = "未选择模型文件夹"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let securityScoped = pickedURL.startAccessingSecurityScopedResource()
|
||||||
|
defer {
|
||||||
|
if securityScoped {
|
||||||
|
pickedURL.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceURL = resolvedModelSourceURL(from: pickedURL)
|
||||||
|
guard FileManager.default.fileExists(
|
||||||
|
atPath: sourceURL.appendingPathComponent(ModelKind.llm.sentinelFilename).path
|
||||||
|
) else {
|
||||||
|
importError = "请选择包含 config.json 的 Qwen3-1.7B-4bit 文件夹"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetURL = ModelStore.shared.localURL(for: .llm)
|
||||||
|
let parentURL = targetURL.deletingLastPathComponent()
|
||||||
|
try FileManager.default.createDirectory(at: parentURL, withIntermediateDirectories: true)
|
||||||
|
if FileManager.default.fileExists(atPath: targetURL.path) {
|
||||||
|
try FileManager.default.removeItem(at: targetURL)
|
||||||
|
}
|
||||||
|
try FileManager.default.copyItem(at: sourceURL, to: targetURL)
|
||||||
|
importError = nil
|
||||||
|
} catch {
|
||||||
|
importError = "导入失败:\(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedModelSourceURL(from pickedURL: URL) -> URL {
|
||||||
|
let nestedURL = pickedURL.appendingPathComponent(ModelKind.llm.rawValue, isDirectory: true)
|
||||||
|
if FileManager.default.fileExists(
|
||||||
|
atPath: nestedURL.appendingPathComponent(ModelKind.llm.sentinelFilename).path
|
||||||
|
) {
|
||||||
|
return nestedURL
|
||||||
|
}
|
||||||
|
return pickedURL
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func run() async {
|
private func run() async {
|
||||||
running = true
|
running = true
|
||||||
|
|||||||
Reference in New Issue
Block a user