Skip to content

Commit 2f9423b

Browse files
lora fixes - to match up with kotlin
1 parent a636697 commit 2f9423b

8 files changed

Lines changed: 222 additions & 104 deletions

File tree

examples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.pbxproj

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
/* Begin PBXBuildFile section */
1010
541C59DA2E63772A00DD7839 /* RunAnywhere in Frameworks */ = {isa = PBXBuildFile; productRef = 541C59D92E63772A00DD7839 /* RunAnywhere */; };
11-
54398AC52F492D1D009D6B51 /* RunAnywhereKeyboard.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54398ABE2F492D1D009D6B51 /* RunAnywhereKeyboard.appex */; platformFilters = (ios, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
11+
54398AC52F492D1D009D6B51 /* RunAnywhereKeyboard.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54398ABE2F492D1D009D6B51 /* RunAnywhereKeyboard.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1212
54398D222F4939A7009D6B51 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54398D212F4939A7009D6B51 /* WidgetKit.framework */; };
1313
54398D242F4939A7009D6B51 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54398D232F4939A7009D6B51 /* SwiftUI.framework */; };
14-
54398D332F4939A7009D6B51 /* RunAnywhereActivityExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54398D202F4939A7009D6B51 /* RunAnywhereActivityExtensionExtension.appex */; platformFilters = (ios, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
14+
54398D332F4939A7009D6B51 /* RunAnywhereActivityExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54398D202F4939A7009D6B51 /* RunAnywhereActivityExtensionExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1515
58ABEDD22ED16DA40058D033 /* RunAnywhereONNX in Frameworks */ = {isa = PBXBuildFile; productRef = 58ABEDD12ED16DA40058D033 /* RunAnywhereONNX */; };
1616
58LLAMACPP12ED16DA40058D0 /* RunAnywhereLlamaCPP in Frameworks */ = {isa = PBXBuildFile; productRef = 58LLAMACPP02ED16DA40058D0 /* RunAnywhereLlamaCPP */; };
1717
58WHISPERKIT1ED16DA40058D0 /* RunAnywhereWhisperKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58WHISPERKIT0ED16DA40058D0 /* RunAnywhereWhisperKit */; };
@@ -542,17 +542,13 @@
542542
/* Begin PBXTargetDependency section */
543543
54398AC42F492D1D009D6B51 /* PBXTargetDependency */ = {
544544
isa = PBXTargetDependency;
545-
platformFilters = (
546-
ios,
547-
);
545+
platformFilter = ios;
548546
target = 54398ABD2F492D1D009D6B51 /* RunAnywhereKeyboard */;
549547
targetProxy = 54398AC32F492D1D009D6B51 /* PBXContainerItemProxy */;
550548
};
551549
54398D322F4939A7009D6B51 /* PBXTargetDependency */ = {
552550
isa = PBXTargetDependency;
553-
platformFilters = (
554-
ios,
555-
);
551+
platformFilter = ios;
556552
target = 54398D1F2F4939A7009D6B51 /* RunAnywhereActivityExtensionExtension */;
557553
targetProxy = 54398D312F4939A7009D6B51 /* PBXContainerItemProxy */;
558554
};

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Models/DemoLoRAAdapter.swift

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,45 +25,47 @@ enum LoRAAdapterCatalog {
2525
}
2626

2727
/// All hardcoded adapters (matches Android's ModelList.kt)
28+
/// All adapters are from Void2377/Qwen HuggingFace repo — trained on Qwen 2.5 0.5B.
2829
static let adapters: [LoraAdapterCatalogEntry] = [
30+
// --- Adapters matching Android's ModelList.kt ---
2931
LoraAdapterCatalogEntry(
30-
id: "chat-assistant-lora",
31-
name: "Chat Assistant",
32-
description: "Enhances conversational chat ability",
33-
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/chat_assistant-lora-Q8_0.gguf")!,
34-
filename: "chat_assistant-lora-Q8_0.gguf",
35-
compatibleModelIds: ["lfm2-350m-q4_k_m", "lfm2-350m-q8_0"],
36-
fileSize: 690_176,
32+
id: "code-assistant-lora",
33+
name: "Code Assistant",
34+
description: "Enhances code generation and programming assistance",
35+
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/code-assistant-Q8_0.gguf")!,
36+
filename: "code-assistant-Q8_0.gguf",
37+
compatibleModelIds: ["qwen2.5-0.5b-instruct-q6_k"],
38+
fileSize: 765_952,
3739
defaultScale: 1.0
3840
),
3941
LoraAdapterCatalogEntry(
40-
id: "summarizer-lora",
41-
name: "Summarizer",
42-
description: "Specialized for text summarization tasks",
43-
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/summarizer-lora-Q8_0.gguf")!,
44-
filename: "summarizer-lora-Q8_0.gguf",
45-
compatibleModelIds: ["lfm2-350m-q4_k_m", "lfm2-350m-q8_0"],
46-
fileSize: 690_176,
42+
id: "reasoning-logic-lora",
43+
name: "Reasoning Logic",
44+
description: "Improves logical reasoning and step-by-step problem solving",
45+
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/reasoning-logic-Q8_0.gguf")!,
46+
filename: "reasoning-logic-Q8_0.gguf",
47+
compatibleModelIds: ["qwen2.5-0.5b-instruct-q6_k"],
48+
fileSize: 765_952,
4749
defaultScale: 1.0
4850
),
4951
LoraAdapterCatalogEntry(
50-
id: "translator-lora",
51-
name: "Translator",
52-
description: "Improves translation between languages",
53-
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/translator-lora-Q8_0.gguf")!,
54-
filename: "translator-lora-Q8_0.gguf",
55-
compatibleModelIds: ["lfm2-350m-q4_k_m", "lfm2-350m-q8_0"],
56-
fileSize: 690_176,
52+
id: "medical-qa-lora",
53+
name: "Medical QA",
54+
description: "Enhances medical question answering and health-related responses",
55+
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/medical-qa-Q8_0.gguf")!,
56+
filename: "medical-qa-Q8_0.gguf",
57+
compatibleModelIds: ["qwen2.5-0.5b-instruct-q6_k"],
58+
fileSize: 765_952,
5759
defaultScale: 1.0
5860
),
5961
LoraAdapterCatalogEntry(
60-
id: "sentiment-lora",
61-
name: "Sentiment Analysis",
62-
description: "Fine-tuned for sentiment analysis tasks",
63-
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/sentiment-lora-Q8_0.gguf")!,
64-
filename: "sentiment-lora-Q8_0.gguf",
65-
compatibleModelIds: ["lfm2-350m-q4_k_m", "lfm2-350m-q8_0"],
66-
fileSize: 690_176,
62+
id: "creative-writing-lora",
63+
name: "Creative Writing",
64+
description: "Improves creative writing, storytelling, and literary style",
65+
downloadURL: URL(string: "https://huggingface.co/Void2377/Qwen/resolve/main/lora/creative-writing-Q8_0.gguf")!,
66+
filename: "creative-writing-Q8_0.gguf",
67+
compatibleModelIds: ["qwen2.5-0.5b-instruct-q6_k"],
68+
fileSize: 765_952,
6769
defaultScale: 1.0
6870
),
6971
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// LoraExamplePrompts.swift
3+
// RunAnywhereAI
4+
//
5+
// Example prompts for each LoRA adapter, keyed by adapter filename.
6+
// Shown in the loaded LoRA adapter card so users can quickly test the adapter.
7+
// Matches Android's LoraExamplePrompts.kt.
8+
9+
import Foundation
10+
11+
enum LoraExamplePrompts {
12+
13+
private static let promptsByFilename: [String: [String]] = [
14+
"code-assistant-Q8_0.gguf": [
15+
"Write a Python function to reverse a linked list",
16+
"Explain the difference between a stack and a queue with code examples",
17+
],
18+
"reasoning-logic-Q8_0.gguf": [
19+
"If all roses are flowers and some flowers fade quickly, can we conclude some roses fade quickly?",
20+
"A farmer has 17 sheep. All but 9 die. How many are left?",
21+
],
22+
"medical-qa-Q8_0.gguf": [
23+
"What are the common symptoms of vitamin D deficiency?",
24+
"Explain the difference between Type 1 and Type 2 diabetes",
25+
],
26+
"creative-writing-Q8_0.gguf": [
27+
"Write a short story about a robot discovering emotions for the first time",
28+
"Describe a sunset over the ocean using vivid sensory language",
29+
],
30+
]
31+
32+
/// Get example prompts for a loaded adapter by its file path.
33+
static func forAdapterPath(_ path: String) -> [String] {
34+
let filename = URL(fileURLWithPath: path).lastPathComponent
35+
return promptsByFilename[filename] ?? []
36+
}
37+
}

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,18 +414,29 @@ final class LLMViewModel {
414414
try FileManager.default.createDirectory(at: loraDir, withIntermediateDirectories: true)
415415
let destinationURL = loraDir.appendingPathComponent(adapter.filename)
416416

417-
if FileManager.default.fileExists(atPath: destinationURL.path) {
417+
if FileManager.default.fileExists(atPath: destinationURL.path),
418+
Self.isValidGGUF(at: destinationURL) {
418419
downloadedAdapterPaths[adapter.id] = destinationURL.path
419420
return destinationURL.path
420421
}
421422

423+
// Remove any previously corrupted download
424+
try? FileManager.default.removeItem(at: destinationURL)
425+
422426
let delegate = DownloadProgressDelegate { [weak self] progress in
423427
Task { @MainActor in
424428
self?.adapterDownloadProgress[adapter.id] = progress
425429
}
426430
}
427431

428432
let (tempURL, _) = try await URLSession.shared.download(from: adapter.downloadURL, delegate: delegate)
433+
434+
// Validate GGUF magic bytes before saving
435+
guard Self.isValidGGUF(at: tempURL) else {
436+
try? FileManager.default.removeItem(at: tempURL)
437+
throw LLMError.custom("Downloaded file is not a valid GGUF adapter (server may have returned an error page)")
438+
}
439+
429440
if FileManager.default.fileExists(atPath: destinationURL.path) {
430441
try FileManager.default.removeItem(at: destinationURL)
431442
}
@@ -436,6 +447,14 @@ final class LLMViewModel {
436447
return destinationURL.path
437448
}
438449

450+
/// Checks that a file starts with the GGUF magic bytes (0x47475546 = "GGUF").
451+
private static func isValidGGUF(at url: URL) -> Bool {
452+
guard let handle = try? FileHandle(forReadingFrom: url) else { return false }
453+
defer { try? handle.close() }
454+
guard let header = try? handle.read(upToCount: 4), header.count == 4 else { return false }
455+
return header == Data([0x47, 0x47, 0x55, 0x46]) // "GGUF"
456+
}
457+
439458
private func syncDownloadedAdapterPaths() {
440459
let loraDir = Self.loraDownloadDirectory()
441460
for adapter in availableAdapters {

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModelTypes.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import Foundation
1111

1212
enum LLMError: LocalizedError {
1313
case noModelLoaded
14+
case custom(String)
1415

1516
var errorDescription: String? {
1617
switch self {
1718
case .noModelLoaded:
1819
return "No model is loaded. Please select and load a model from the Models tab first."
20+
case .custom(let message):
21+
return message
1922
}
2023
}
2124
}

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Views/ChatInterfaceView.swift

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -752,32 +752,63 @@ private struct LoRAManagementSheetView: View {
752752
if !viewModel.loraAdapters.isEmpty {
753753
Section("Loaded Adapters") {
754754
ForEach(viewModel.loraAdapters, id: \.path) { adapter in
755-
HStack {
756-
VStack(alignment: .leading, spacing: 4) {
757-
Text(URL(fileURLWithPath: adapter.path).lastPathComponent)
758-
.font(.subheadline)
759-
.lineLimit(1)
760-
HStack(spacing: 8) {
761-
Text("Scale: \(String(format: "%.1f", adapter.scale))")
762-
.font(.caption)
763-
.foregroundColor(.secondary)
764-
if adapter.applied {
765-
Text("Applied")
755+
VStack(alignment: .leading, spacing: 8) {
756+
HStack {
757+
VStack(alignment: .leading, spacing: 4) {
758+
Text(URL(fileURLWithPath: adapter.path).lastPathComponent)
759+
.font(.subheadline)
760+
.lineLimit(1)
761+
HStack(spacing: 8) {
762+
Text("Scale: \(String(format: "%.1f", adapter.scale))")
766763
.font(.caption)
767-
.foregroundColor(.green)
764+
.foregroundColor(.secondary)
765+
if adapter.applied {
766+
Text("Applied")
767+
.font(.caption)
768+
.foregroundColor(.green)
769+
}
768770
}
769771
}
770-
}
771772

772-
Spacer()
773+
Spacer()
773774

774-
Button {
775-
Task { await viewModel.removeLoraAdapter(path: adapter.path) }
776-
} label: {
777-
Image(systemName: "xmark.circle.fill")
778-
.foregroundColor(.secondary)
775+
Button {
776+
Task { await viewModel.removeLoraAdapter(path: adapter.path) }
777+
} label: {
778+
Image(systemName: "xmark.circle.fill")
779+
.foregroundColor(.secondary)
780+
}
781+
.buttonStyle(.plain)
782+
}
783+
784+
let prompts = LoraExamplePrompts.forAdapterPath(adapter.path)
785+
if !prompts.isEmpty {
786+
VStack(alignment: .leading, spacing: 6) {
787+
Text("Try it out:")
788+
.font(.caption2)
789+
.foregroundColor(.secondary)
790+
ForEach(prompts, id: \.self) { prompt in
791+
Button {
792+
UIPasteboard.general.string = prompt
793+
} label: {
794+
HStack(spacing: 4) {
795+
Image(systemName: "doc.on.doc")
796+
.font(.caption2)
797+
Text(prompt)
798+
.font(.caption)
799+
.lineLimit(2)
800+
.multilineTextAlignment(.leading)
801+
}
802+
.padding(.horizontal, 10)
803+
.padding(.vertical, 6)
804+
.background(Color.purple.opacity(0.15))
805+
.foregroundColor(.purple)
806+
.cornerRadius(8)
807+
}
808+
.buttonStyle(.plain)
809+
}
810+
}
779811
}
780-
.buttonStyle(.plain)
781812
}
782813
}
783814

0 commit comments

Comments
 (0)