68 lines
2.6 KiB
Swift
68 lines
2.6 KiB
Swift
import Foundation
|
|
|
|
/// 单次性能自检结果。按后端标签归档,供「MNN·SME2 vs MLX·GPU」对比展示(§12 卖点 2/6)。
|
|
struct BenchmarkResult: Codable, Equatable {
|
|
var backendLabel: String
|
|
var promptTokens: Int
|
|
var genTokens: Int
|
|
var prefillTokensPerSecond: Double
|
|
var decodeTokensPerSecond: Double
|
|
var totalSeconds: Double
|
|
var date: Date
|
|
}
|
|
|
|
/// 性能自检服务:跑固定 prompt,取 AIRuntime 的归一统计,按后端标签存 UserDefaults。
|
|
/// UI(ModelSelfTestView)只经本服务调 AIRuntime(§3.1)。
|
|
@MainActor
|
|
struct BenchmarkService {
|
|
static let shared = BenchmarkService()
|
|
private init() {}
|
|
|
|
nonisolated static let storeKey = "kk.benchmark.results"
|
|
|
|
/// 固定测试 prompt:跨设备/引擎可比的前提。
|
|
static let fixedPrompt = "用中文一句话介绍肝功能里 ALT 这个指标。"
|
|
|
|
/// 跑一次自检。onToken 把流式输出交给 UI 展示。
|
|
func run(onToken: @escaping @MainActor (String, Double) -> Void) async throws -> BenchmarkResult {
|
|
try await AIRuntime.shared.prepare()
|
|
let start = Date()
|
|
let stream = await AIRuntime.shared.generate(prompt: Self.fixedPrompt, maxTokens: 128)
|
|
for try await chunk in stream {
|
|
onToken(chunk.text, chunk.decodeRate)
|
|
}
|
|
let total = Date().timeIntervalSince(start)
|
|
let label = await AIRuntime.shared.activeBackendLabel
|
|
let stats = await AIRuntime.shared.lastGenerateStats
|
|
let result = BenchmarkResult(
|
|
backendLabel: label,
|
|
promptTokens: stats?.promptTokens ?? 0,
|
|
genTokens: stats?.genTokens ?? 0,
|
|
prefillTokensPerSecond: stats?.prefillTokensPerSecond ?? 0,
|
|
decodeTokensPerSecond: stats?.decodeTokensPerSecond ?? 0,
|
|
totalSeconds: total,
|
|
date: .now
|
|
)
|
|
Self.save(result)
|
|
return result
|
|
}
|
|
|
|
// MARK: - 存档(静态纯函数,单测覆盖;nonisolated:纯 UserDefaults 操作,无需主线程)
|
|
|
|
nonisolated static func save(_ result: BenchmarkResult, defaults: UserDefaults = .standard) {
|
|
var all = load(defaults: defaults)
|
|
all[result.backendLabel] = result
|
|
if let data = try? JSONEncoder().encode(all) {
|
|
defaults.set(data, forKey: storeKey)
|
|
}
|
|
}
|
|
|
|
nonisolated static func load(defaults: UserDefaults = .standard) -> [String: BenchmarkResult] {
|
|
guard let data = defaults.data(forKey: storeKey),
|
|
let all = try? JSONDecoder().decode([String: BenchmarkResult].self, from: data) else {
|
|
return [:]
|
|
}
|
|
return all
|
|
}
|
|
}
|