Some checks failed
CI / build-and-test (push) Has been cancelled
- E4B-MarkBase model (42 layers, 4.4GB) loaded successfully - All Phase 1-6 tests passed (model loading, forward pass, vision/audio towers, token generation, performance) - All stress tests passed (5/5 in 127.6s) - Concurrent inference - Memory stress (67.5 tok/s, 0 NaN) - Continuous generation - Batch processing - Long-running stability - Swift Metal inference engine with multimodal support
411 lines
17 KiB
Swift
411 lines
17 KiB
Swift
import Foundation
|
||
|
||
public struct ComparisonResult {
|
||
public let audioResults: [AudioTestResult]
|
||
public let visionResults: [VisionTestResult]
|
||
public let endToEndResults: [EndToEndResult]
|
||
public let timestamp: Date
|
||
}
|
||
|
||
public struct EndToEndResult {
|
||
public let modelName: String
|
||
public let loadTimeMs: Double
|
||
public let audioProcessMs: Double
|
||
public let visionProcessMs: Double
|
||
public let genSpeedTokPerSec: Double
|
||
public let totalTimeMs: Double
|
||
public let generatedTokens: Int
|
||
public let passed: Bool
|
||
}
|
||
|
||
public final class ComparisonReporter {
|
||
private let outputDir: String
|
||
|
||
public init(outputDir: String = "/tmp/multimodal_comparison") {
|
||
self.outputDir = outputDir
|
||
try? FileManager.default.createDirectory(atPath: outputDir, withIntermediateDirectories: true)
|
||
}
|
||
|
||
public func printAudioTable(results: [AudioTestResult], sampleName: String) {
|
||
print("\n▶ AUDIO TEST: \(sampleName)")
|
||
printTableHeader(columns: ["Metric", "E2B", "E4B", "12B"], widths: [12, 15, 15, 15])
|
||
printTableSeparator(widths: [12, 15, 15, 15])
|
||
|
||
let e2b = results.first { $0.modelName == "E2B" }
|
||
let e4b = results.first { $0.modelName == "E4B" }
|
||
let g12b = results.first { $0.modelName == "12B" }
|
||
|
||
printTableRow(columns: ["Shape",
|
||
e2b != nil ? "[\(e2b!.outputShape.0), \(e2b!.outputShape.1)]" : "N/A",
|
||
e4b != nil ? "[\(e4b!.outputShape.0), \(e4b!.outputShape.1)]" : "N/A",
|
||
g12b != nil ? "[\(g12b!.outputShape.0), \(g12b!.outputShape.1)]" : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Min",
|
||
e2b != nil ? String(format: "%.3f", e2b!.min) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.min) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.min) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Max",
|
||
e2b != nil ? String(format: "%.3f", e2b!.max) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.max) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.max) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Mean",
|
||
e2b != nil ? String(format: "%.3f", e2b!.mean) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.mean) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.mean) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Std",
|
||
e2b != nil ? String(format: "%.3f", e2b!.std) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.std) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.std) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["NaN",
|
||
e2b != nil ? String(e2b!.nanCount) : "N/A",
|
||
e4b != nil ? String(e4b!.nanCount) : "N/A",
|
||
g12b != nil ? String(g12b!.nanCount) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Time",
|
||
e2b != nil ? String(format: "%.1f ms", e2b!.forwardTimeMs) : "N/A",
|
||
e4b != nil ? String(format: "%.1f ms", e4b!.forwardTimeMs) : "N/A",
|
||
g12b != nil ? String(format: "%.1f ms", g12b!.forwardTimeMs) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Memory",
|
||
e2b != nil ? String(format: "%.0f MB", e2b!.memoryPeakMB) : "N/A",
|
||
e4b != nil ? String(format: "%.0f MB", e4b!.memoryPeakMB) : "N/A",
|
||
g12b != nil ? String(format: "%.0f MB", g12b!.memoryPeakMB) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Status",
|
||
e2b != nil ? (e2b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A",
|
||
e4b != nil ? (e4b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A",
|
||
g12b != nil ? (g12b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableSeparator(widths: [12, 15, 15, 15])
|
||
}
|
||
|
||
public func printVisionTable(results: [VisionTestResult], sampleName: String) {
|
||
print("\n▶ VISION TEST: \(sampleName)")
|
||
printTableHeader(columns: ["Metric", "E2B", "E4B", "12B"], widths: [12, 15, 15, 15])
|
||
printTableSeparator(widths: [12, 15, 15, 15])
|
||
|
||
let e2b = results.first { $0.modelName == "E2B" }
|
||
let e4b = results.first { $0.modelName == "E4B" }
|
||
let g12b = results.first { $0.modelName == "12B" }
|
||
|
||
printTableRow(columns: ["Shape",
|
||
e2b != nil ? "[\(e2b!.outputShape.0), \(e2b!.outputShape.1)]" : "N/A",
|
||
e4b != nil ? "[\(e4b!.outputShape.0), \(e4b!.outputShape.1)]" : "N/A",
|
||
g12b != nil ? "[\(g12b!.outputShape.0), \(g12b!.outputShape.1)]" : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Min",
|
||
e2b != nil ? String(format: "%.3f", e2b!.min) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.min) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.min) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Max",
|
||
e2b != nil ? String(format: "%.3f", e2b!.max) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.max) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.max) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Mean",
|
||
e2b != nil ? String(format: "%.3f", e2b!.mean) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.mean) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.mean) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Std",
|
||
e2b != nil ? String(format: "%.3f", e2b!.std) : "N/A",
|
||
e4b != nil ? String(format: "%.3f", e4b!.std) : "N/A",
|
||
g12b != nil ? String(format: "%.3f", g12b!.std) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["NaN",
|
||
e2b != nil ? String(e2b!.nanCount) : "N/A",
|
||
e4b != nil ? String(e4b!.nanCount) : "N/A",
|
||
g12b != nil ? String(g12b!.nanCount) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["CosSim",
|
||
"—",
|
||
e4b != nil && e2b != nil ? String(format: "%.3f", e4b!.cosineSimilarity ?? 0) : "N/A",
|
||
"—"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Time",
|
||
e2b != nil ? String(format: "%.1f ms", e2b!.forwardTimeMs) : "N/A",
|
||
e4b != nil ? String(format: "%.1f ms", e4b!.forwardTimeMs) : "N/A",
|
||
g12b != nil ? String(format: "%.1f ms", g12b!.forwardTimeMs) : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableRow(columns: ["Status",
|
||
e2b != nil ? (e2b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A",
|
||
e4b != nil ? (e4b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A",
|
||
g12b != nil ? (g12b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A"
|
||
], widths: [12, 15, 15, 15])
|
||
|
||
printTableSeparator(widths: [12, 15, 15, 15])
|
||
}
|
||
|
||
public func printEndToEndTable(results: [EndToEndResult]) {
|
||
print("\n▶ END-TO-END TEST: audio+vision generation")
|
||
printTableHeader(columns: ["Metric", "E2B", "E4B", "12B"], widths: [14, 12, 12, 12])
|
||
printTableSeparator(widths: [14, 12, 12, 12])
|
||
|
||
let e2b = results.first { $0.modelName == "E2B" }
|
||
let e4b = results.first { $0.modelName == "E4B" }
|
||
let g12b = results.first { $0.modelName == "12B" }
|
||
|
||
printTableRow(columns: ["Load Time",
|
||
e2b != nil ? String(format: "%.2f s", e2b!.loadTimeMs / 1000.0) : "N/A",
|
||
e4b != nil ? String(format: "%.2f s", e4b!.loadTimeMs / 1000.0) : "N/A",
|
||
g12b != nil ? String(format: "%.2f s", g12b!.loadTimeMs / 1000.0) : "N/A"
|
||
], widths: [14, 12, 12, 12])
|
||
|
||
printTableRow(columns: ["Audio Proc",
|
||
e2b != nil ? String(format: "%.1f ms", e2b!.audioProcessMs) : "N/A",
|
||
e4b != nil ? String(format: "%.1f ms", e4b!.audioProcessMs) : "N/A",
|
||
g12b != nil ? String(format: "%.1f ms", g12b!.audioProcessMs) : "N/A"
|
||
], widths: [14, 12, 12, 12])
|
||
|
||
printTableRow(columns: ["Vision Proc",
|
||
e2b != nil ? String(format: "%.1f ms", e2b!.visionProcessMs) : "N/A",
|
||
e4b != nil ? String(format: "%.1f ms", e4b!.visionProcessMs) : "N/A",
|
||
g12b != nil ? String(format: "%.1f ms", g12b!.visionProcessMs) : "N/A"
|
||
], widths: [14, 12, 12, 12])
|
||
|
||
printTableRow(columns: ["Gen Speed",
|
||
e2b != nil ? String(format: "%.1f t/s", e2b!.genSpeedTokPerSec) : "N/A",
|
||
e4b != nil ? String(format: "%.1f t/s", e4b!.genSpeedTokPerSec) : "N/A",
|
||
g12b != nil ? String(format: "%.1f t/s", g12b!.genSpeedTokPerSec) : "N/A"
|
||
], widths: [14, 12, 12, 12])
|
||
|
||
printTableRow(columns: ["Total Time",
|
||
e2b != nil ? String(format: "%.2f s", e2b!.totalTimeMs / 1000.0) : "N/A",
|
||
e4b != nil ? String(format: "%.2f s", e4b!.totalTimeMs / 1000.0) : "N/A",
|
||
g12b != nil ? String(format: "%.2f s", g12b!.totalTimeMs / 1000.0) : "N/A"
|
||
], widths: [14, 12, 12, 12])
|
||
|
||
printTableRow(columns: ["Status",
|
||
e2b != nil ? (e2b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A",
|
||
e4b != nil ? (e4b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A",
|
||
g12b != nil ? (g12b!.passed ? "✓ PASS" : "✗ FAIL") : "N/A"
|
||
], widths: [14, 12, 12, 12])
|
||
|
||
printTableSeparator(widths: [14, 12, 12, 12])
|
||
}
|
||
|
||
public func printSummary() {
|
||
print("\n═══════════════════════════════════════════════════════════════════")
|
||
print(" SUMMARY")
|
||
print("═══════════════════════════════════════════════════════════════════")
|
||
print("Audio Tower: E2B/E4B 完整12層,12B僅投影")
|
||
print("Vision Tower: E2B/E4B 完整16層,12B簡化版")
|
||
print("Speed: 12B最快,但品質較差")
|
||
print("Quality: E4B最佳(量化精度平衡)")
|
||
print("Memory: E2B最小(2B參數)")
|
||
print("═══════════════════════════════════════════════════════════════════\n")
|
||
}
|
||
|
||
private func printTableHeader(columns: [String], widths: [Int]) {
|
||
var line = "┌"
|
||
for (i, w) in widths.enumerated() {
|
||
line += String(repeating: "─", count: w)
|
||
if i < widths.count - 1 { line += "┬" }
|
||
}
|
||
line += "┐"
|
||
print(line)
|
||
|
||
var row = "│"
|
||
for i in 0..<columns.count {
|
||
let col = columns[i]
|
||
let w = widths[i]
|
||
row += padRight(col, width: w)
|
||
if i < widths.count - 1 { row += "│" }
|
||
}
|
||
row += "│"
|
||
print(row)
|
||
}
|
||
|
||
private func printTableSeparator(widths: [Int]) {
|
||
var line = "├"
|
||
for (i, w) in widths.enumerated() {
|
||
line += String(repeating: "─", count: w)
|
||
if i < widths.count - 1 { line += "┼" }
|
||
}
|
||
line += "┤"
|
||
print(line)
|
||
}
|
||
|
||
private func printTableRow(columns: [String], widths: [Int]) {
|
||
var row = "│"
|
||
for i in 0..<columns.count {
|
||
let col = columns[i]
|
||
let w = widths[i]
|
||
row += padRight(col, width: w)
|
||
if i < widths.count - 1 { row += "│" }
|
||
}
|
||
row += "│"
|
||
print(row)
|
||
}
|
||
|
||
private func padRight(_ s: String, width: Int) -> String {
|
||
if s.count >= width { return s }
|
||
return s + String(repeating: " ", count: width - s.count)
|
||
}
|
||
|
||
public func writeJSON(result: ComparisonResult) throws {
|
||
let encoder = JSONEncoder()
|
||
encoder.outputFormatting = .prettyPrinted
|
||
let data = try encoder.encode(JSONReport(from: result))
|
||
let path = outputDir + "/comparison_results.json"
|
||
FileManager.default.createFile(atPath: path, contents: data)
|
||
print(" JSON report saved to: \(path)")
|
||
}
|
||
|
||
public func writeCSV(result: ComparisonResult) throws {
|
||
var csv = "type,sample,model,metric,value\n"
|
||
|
||
for r in result.audioResults {
|
||
csv += "audio,\(r.sampleName),\(r.modelName),min,\(r.min)\n"
|
||
csv += "audio,\(r.sampleName),\(r.modelName),max,\(r.max)\n"
|
||
csv += "audio,\(r.sampleName),\(r.modelName),mean,\(r.mean)\n"
|
||
csv += "audio,\(r.sampleName),\(r.modelName),std,\(r.std)\n"
|
||
csv += "audio,\(r.sampleName),\(r.modelName),nan_count,\(r.nanCount)\n"
|
||
csv += "audio,\(r.sampleName),\(r.modelName),time_ms,\(r.forwardTimeMs)\n"
|
||
csv += "audio,\(r.sampleName),\(r.modelName),memory_mb,\(r.memoryPeakMB)\n"
|
||
csv += "audio,\(r.sampleName),\(r.modelName),passed,\(r.passed)\n"
|
||
}
|
||
|
||
for r in result.visionResults {
|
||
csv += "vision,\(r.sampleName),\(r.modelName),min,\(r.min)\n"
|
||
csv += "vision,\(r.sampleName),\(r.modelName),max,\(r.max)\n"
|
||
csv += "vision,\(r.sampleName),\(r.modelName),mean,\(r.mean)\n"
|
||
csv += "vision,\(r.sampleName),\(r.modelName),std,\(r.std)\n"
|
||
csv += "vision,\(r.sampleName),\(r.modelName),nan_count,\(r.nanCount)\n"
|
||
if let cs = r.cosineSimilarity {
|
||
csv += "vision,\(r.sampleName),\(r.modelName),cosine_sim,\(cs)\n"
|
||
}
|
||
csv += "vision,\(r.sampleName),\(r.modelName),time_ms,\(r.forwardTimeMs)\n"
|
||
csv += "vision,\(r.sampleName),\(r.modelName),passed,\(r.passed)\n"
|
||
}
|
||
|
||
for r in result.endToEndResults {
|
||
csv += "e2e,all,\(r.modelName),load_time_ms,\(r.loadTimeMs)\n"
|
||
csv += "e2e,all,\(r.modelName),audio_proc_ms,\(r.audioProcessMs)\n"
|
||
csv += "e2e,all,\(r.modelName),vision_proc_ms,\(r.visionProcessMs)\n"
|
||
csv += "e2e,all,\(r.modelName),gen_speed_tok_s,\(r.genSpeedTokPerSec)\n"
|
||
csv += "e2e,all,\(r.modelName),total_time_ms,\(r.totalTimeMs)\n"
|
||
csv += "e2e,all,\(r.modelName),passed,\(r.passed)\n"
|
||
}
|
||
|
||
let path = outputDir + "/comparison_results.csv"
|
||
FileManager.default.createFile(atPath: path, contents: csv.data(using: .utf8))
|
||
print(" CSV report saved to: \(path)")
|
||
}
|
||
}
|
||
|
||
private struct JSONReport: Codable {
|
||
let timestamp: String
|
||
let audio: [AudioResultJSON]
|
||
let vision: [VisionResultJSON]
|
||
let endToEnd: [EndToEndJSON]
|
||
|
||
init(from result: ComparisonResult) {
|
||
timestamp = ISO8601DateFormatter().string(from: result.timestamp)
|
||
audio = result.audioResults.map { AudioResultJSON(from: $0) }
|
||
vision = result.visionResults.map { VisionResultJSON(from: $0) }
|
||
endToEnd = result.endToEndResults.map { EndToEndJSON(from: $0) }
|
||
}
|
||
}
|
||
|
||
private struct AudioResultJSON: Codable {
|
||
let modelName: String
|
||
let sampleName: String
|
||
let outputRows: Int
|
||
let outputCols: Int
|
||
let min: Float
|
||
let max: Float
|
||
let mean: Float
|
||
let std: Float
|
||
let nanCount: Int
|
||
let forwardTimeMs: Double
|
||
let memoryPeakMB: Double
|
||
let passed: Bool
|
||
|
||
init(from r: AudioTestResult) {
|
||
modelName = r.modelName
|
||
sampleName = r.sampleName
|
||
outputRows = r.outputShape.0
|
||
outputCols = r.outputShape.1
|
||
min = r.min
|
||
max = r.max
|
||
mean = r.mean
|
||
std = r.std
|
||
nanCount = r.nanCount
|
||
forwardTimeMs = r.forwardTimeMs
|
||
memoryPeakMB = r.memoryPeakMB
|
||
passed = r.passed
|
||
}
|
||
}
|
||
|
||
private struct VisionResultJSON: Codable {
|
||
let modelName: String
|
||
let sampleName: String
|
||
let outputRows: Int
|
||
let outputCols: Int
|
||
let min: Float
|
||
let max: Float
|
||
let mean: Float
|
||
let std: Float
|
||
let nanCount: Int
|
||
let cosineSimilarity: Float?
|
||
let forwardTimeMs: Double
|
||
let passed: Bool
|
||
|
||
init(from r: VisionTestResult) {
|
||
modelName = r.modelName
|
||
sampleName = r.sampleName
|
||
outputRows = r.outputShape.0
|
||
outputCols = r.outputShape.1
|
||
min = r.min
|
||
max = r.max
|
||
mean = r.mean
|
||
std = r.std
|
||
nanCount = r.nanCount
|
||
cosineSimilarity = r.cosineSimilarity
|
||
forwardTimeMs = r.forwardTimeMs
|
||
passed = r.passed
|
||
}
|
||
}
|
||
|
||
private struct EndToEndJSON: Codable {
|
||
let modelName: String
|
||
let loadTimeMs: Double
|
||
let audioProcessMs: Double
|
||
let visionProcessMs: Double
|
||
let genSpeedTokPerSec: Double
|
||
let totalTimeMs: Double
|
||
let passed: Bool
|
||
|
||
init(from r: EndToEndResult) {
|
||
modelName = r.modelName
|
||
loadTimeMs = r.loadTimeMs
|
||
audioProcessMs = r.audioProcessMs
|
||
visionProcessMs = r.visionProcessMs
|
||
genSpeedTokPerSec = r.genSpeedTokPerSec
|
||
totalTimeMs = r.totalTimeMs
|
||
passed = r.passed
|
||
}
|
||
} |