spb/zyquo-mlx Public MIT
The local MLX foundry for your Mac — run, fine-tune, quantize, and ship models. Nothing leaves your machine.
Swift 93.4%
Python 3.8%
Makefile 2.2%
Shell 0.5%
1//2// EvaluateView.swift3// Zyquo MLX4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import SwiftUI1011/// Evaluate section: base-vs-tuned side-by-side qualitative comparison with12/// per-side stats (held-out perplexity via the Python bridge uses the same13/// engine as `--test`; qualitative compare is the flagship workflow).14struct EvaluateView: View {15 @Bindable var model: AppModel16 @State private var controller = EvaluateController()1718 var body: some View {19 if model.models.count < 2 {20 EmptyStateView(21 icon: "checkmark.seal",22 title: "Prove Your Fine-Tune",23 message: "Once you have a base model and a fine-tuned (fused) variant, compare them side by side on the same prompt — with tokens/sec and TTFT for each.")24 } else {25 VStack(spacing: 0) {26 HStack(spacing: ZyquoTheme.spacing12) {27 Picker("Base", selection: $controller.baseID) {28 Text("Choose…").tag(String?.none)29 ForEach(model.models.filter { $0.type == .llm }) {30 Text($0.name).tag(String?.some($0.id))31 }32 }33 Picker("Candidate", selection: $controller.candidateID) {34 Text("Choose…").tag(String?.none)35 ForEach(model.models.filter { $0.type == .llm }) {36 Text($0.name).tag(String?.some($0.id))37 }38 }39 }40 .padding(ZyquoTheme.spacing12)4142 HStack(spacing: ZyquoTheme.spacing8) {43 TextField("Prompt to compare on…", text: $controller.prompt)44 .textFieldStyle(.roundedBorder)45 .onSubmit { controller.run(models: model.models) }46 Button(controller.isRunning ? "Running…" : "Compare") {47 controller.run(models: model.models)48 }49 .buttonStyle(.borderedProminent)50 .tint(ZyquoTheme.accent)51 .disabled(controller.isRunning || controller.baseID == nil52 || controller.candidateID == nil || controller.prompt.isEmpty)53 }54 .padding(.horizontal, ZyquoTheme.spacing12)55 .padding(.bottom, ZyquoTheme.spacing12)5657 Rectangle().fill(ZyquoTheme.border).frame(height: ZyquoTheme.hairline)5859 HStack(alignment: .top, spacing: 0) {60 ResultColumn(title: "Base", output: controller.baseOutput, stats: controller.baseStats)61 Rectangle().fill(ZyquoTheme.border).frame(width: ZyquoTheme.hairline)62 ResultColumn(title: "Candidate", output: controller.candidateOutput, stats: controller.candidateStats)63 }6465 if let error = controller.errorMessage {66 Text(error)67 .font(ZyquoTheme.captionFont)68 .foregroundStyle(ZyquoTheme.danger)69 .padding(ZyquoTheme.spacing8)70 }71 }72 }73 }74}7576private struct ResultColumn: View {77 let title: String78 let output: String79 let stats: InferenceStats?8081 var body: some View {82 VStack(alignment: .leading, spacing: ZyquoTheme.spacing8) {83 HStack {84 Text(title)85 .font(ZyquoTheme.headlineFont)86 Spacer()87 if let stats {88 Text(String(format: "%.1f tok/s · TTFT %.2fs", stats.tokensPerSecond, stats.ttft))89 .font(ZyquoTheme.monoSmallFont)90 .foregroundStyle(ZyquoTheme.chartThroughput)91 }92 }93 ScrollView {94 Text(output.isEmpty ? "—" : output)95 .font(ZyquoTheme.bodyFont)96 .foregroundStyle(ZyquoTheme.textPrimary)97 .textSelection(.enabled)98 .frame(maxWidth: .infinity, alignment: .leading)99 }100 }101 .padding(ZyquoTheme.spacing16)102 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)103 }104}105106@Observable107@MainActor108final class EvaluateController {109 var baseID: String?110 var candidateID: String?111 var prompt = "Explain what a LoRA adapter is in one sentence."112 var baseOutput = ""113 var candidateOutput = ""114 var baseStats: InferenceStats?115 var candidateStats: InferenceStats?116 var isRunning = false117 var errorMessage: String?118119 /// Sequential run (one model in memory at a time — verifiable unload120 /// between the two sides).121 func run(models: [LocalModel]) {122 guard let base = models.first(where: { $0.id == baseID }),123 let candidate = models.first(where: { $0.id == candidateID })124 else { return }125 isRunning = true126 errorMessage = nil127 baseOutput = ""128 candidateOutput = ""129130 Task {131 do {132 (baseOutput, baseStats) = try await generate(model: base)133 (candidateOutput, candidateStats) = try await generate(model: candidate)134 } catch {135 errorMessage = error.localizedDescription136 }137 _ = try? await InferenceEngine.shared.unload()138 isRunning = false139 }140 }141142 private func generate(model: LocalModel) async throws -> (String, InferenceStats?) {143 try await InferenceEngine.shared.load(model: model)144 var output = ""145 var stats: InferenceStats?146 var params = GenerationParams()147 params.maxTokens = 512148 let stream = try await InferenceEngine.shared.generate(149 messages: [.user(prompt)], params: params)150 for try await event in stream {151 switch event {152 case .chunk(let piece): output += piece153 case .finished(let s): stats = s154 }155 }156 return (output, stats)157 }158}159