Files
markbaseengine/Tests/MarkBaseTests/ComparisonReporter.swift
MarkBase Admin ac75faa0cc
Some checks failed
CI / build-and-test (push) Has been cancelled
Initial commit: E4B-MarkBase model integration with passing tests
- 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
2026-06-23 18:12:35 +08:00

411 lines
17 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}