diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/App.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/App.swift new file mode 100644 index 00000000000..442e320f30c --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/App.swift @@ -0,0 +1,12 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import SwiftUI + +@main +struct App: SwiftUI.App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ContentView.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ContentView.swift new file mode 100644 index 00000000000..456e8369bbf --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ContentView.swift @@ -0,0 +1,273 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import SwiftUI +import UniformTypeIdentifiers + +import LLaMARunner + +class RunnerHolder: ObservableObject { + var runner: Runner? +} + +struct ContentView: View { + @State private var prompt = "" + @State private var messages: [Message] = [] + @State private var showingLogs = false + @State private var pickerType: PickerType? + @State private var isGenerating = false + @State private var shouldStopGenerating = false + private let runnerQueue = DispatchQueue(label: "org.pytorch.executorch.llama") + @StateObject private var runnerHolder = RunnerHolder() + @StateObject private var resourceManager = ResourceManager() + @StateObject private var resourceMonitor = ResourceMonitor() + @StateObject private var logManager = LogManager() + + enum PickerType { + case model + case tokenizer + } + + private var placeholder: String { + resourceManager.isModelValid ? resourceManager.isTokenizerValid ? "Prompt..." : "Select Tokenizer..." : "Select Model..." + } + + private var title: String { + resourceManager.isModelValid ? resourceManager.isTokenizerValid ? resourceManager.modelName : "Select Tokenizer..." : "Select Model..." + } + + private var modelTitle: String { + resourceManager.isModelValid ? resourceManager.modelName : "Select Model..." + } + + private var tokenizerTitle: String { + resourceManager.isTokenizerValid ? resourceManager.tokenizerName : "Select Tokenizer..." + } + + private var isInputEnabled: Bool { resourceManager.isModelValid && resourceManager.isTokenizerValid } + + var body: some View { + NavigationView { + VStack { + MessageListView(messages: $messages) + .gesture( + DragGesture().onChanged { value in + if value.translation.height > 10 { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + } + ) + HStack { + Menu { + Section(header: Text("Model")) { + Button(action: { pickerType = .model }) { + Label(modelTitle, systemImage: "doc") + } + } + Section(header: Text("Tokenizer")) { + Button(action: { pickerType = .tokenizer }) { + Label(tokenizerTitle, systemImage: "doc") + } + } + } label: { + Image(systemName: "ellipsis.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 28) + } + .disabled(isGenerating) + + TextField(placeholder, text: $prompt, axis: .vertical) + .padding(8) + .background(Color.gray.opacity(0.1)) + .cornerRadius(20) + .lineLimit(1...10) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(isInputEnabled ? Color.blue : Color.gray, lineWidth: 1) + ) + .disabled(!isInputEnabled) + + Button(action: isGenerating ? stop : generate) { + Image(systemName: isGenerating ? "stop.circle" : "arrowshape.up.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 28) + } + .disabled(isGenerating ? shouldStopGenerating : (!isInputEnabled || prompt.isEmpty)) + } + .padding([.leading, .trailing, .bottom], 10) + } + .navigationBarTitle(title, displayMode: .inline) + .navigationBarItems(trailing: + HStack { + Menu { + Section(header: Text("Memory")) { + Text("Used: \(resourceMonitor.usedMemory) Mb") + Text("Available: \(resourceMonitor.availableMemory) Mb") + } + } label: { + Text("\(resourceMonitor.usedMemory) Mb") + } + .onAppear { + resourceMonitor.start() + } + .onDisappear { + resourceMonitor.stop() + } + Button(action: { showingLogs = true }) { + Image(systemName: "list.bullet.rectangle") + } + } + ) + .sheet(isPresented: $showingLogs) { + NavigationView { + LogView(logManager: logManager) + } + } + .fileImporter( + isPresented: Binding( + get: { pickerType != nil }, + set: { if !$0 { pickerType = nil } } + ), + allowedContentTypes: allowedContentTypes(), + allowsMultipleSelection: false + ) { [pickerType] result in + handleFileImportResult(pickerType, result) + } + .onAppear { + do { + try resourceManager.createDirectoriesIfNeeded() + } catch { + withAnimation { + messages.append(Message(type: .info, text: "Error creating content directories: \(error.localizedDescription)")) + } + } + } + } + } + + private func generate() { + guard !prompt.isEmpty else { return } + isGenerating = true + shouldStopGenerating = false + let text = prompt.trimmingCharacters(in: .whitespacesAndNewlines) + let seq_len = 128 + prompt = "" + let modelPath = resourceManager.modelPath + let tokenizerPath = resourceManager.tokenizerPath + + messages.append(Message(text: text)) + messages.append(Message(type: .generated)) + + runnerQueue.async { + defer { + DispatchQueue.main.async { + isGenerating = false + } + } + runnerHolder.runner = runnerHolder.runner ?? Runner(modelPath: modelPath, tokenizerPath: tokenizerPath) + guard !shouldStopGenerating else { return } + if let runner = runnerHolder.runner, !runner.isloaded() { + var error: Error? + let startLoadTime = Date() + do { + try runner.load() + } catch let loadError { + error = loadError + } + let loadTime = Date().timeIntervalSince(startLoadTime) + DispatchQueue.main.async { + withAnimation { + var message = messages.removeLast() + message.type = .info + if let error { + message.text = "Model loading failed: error \((error as NSError).code)" + } else { + message.text = "Model loaded in \(String(format: "%.1f", loadTime)) s" + } + messages.append(message) + if error == nil { + messages.append(Message(type: .generated)) + } + } + } + if error != nil { + return + } + } + guard !shouldStopGenerating else { + DispatchQueue.main.async { + withAnimation { + _ = messages.removeLast() + } + } + return + } + do { + try runnerHolder.runner?.generate(text, sequenceLength: seq_len) { token in + + DispatchQueue.main.async { + withAnimation { + var message = messages.removeLast() + message.text += token + message.tokenCount += 1 + message.dateUpdated = Date() + messages.append(message) + } + } + if shouldStopGenerating { + runnerHolder.runner?.stop() + } + } + } catch { + DispatchQueue.main.async { + withAnimation { + var message = messages.removeLast() + message.type = .info + message.text = "Text generation failed: error \((error as NSError).code)" + messages.append(message) + } + } + } + } + } + + private func stop() { + shouldStopGenerating = true + } + + private func allowedContentTypes() -> [UTType] { + guard let pickerType else { return [] } + switch pickerType { + case .model: + return [UTType(filenameExtension: "pte")].compactMap { $0 } + case .tokenizer: + return [UTType(filenameExtension: "bin")].compactMap { $0 } + } + } + + private func handleFileImportResult(_ pickerType: PickerType?, _ result: Result<[URL], Error>) { + switch result { + case .success(let urls): + guard let url = urls.first, let pickerType else { + withAnimation { + messages.append(Message(type: .info, text: "Failed to select a file")) + } + return + } + runnerQueue.async { + runnerHolder.runner = nil + } + switch pickerType { + case .model: + resourceManager.modelPath = url.path + case .tokenizer: + resourceManager.tokenizerPath = url.path + } + case .failure(let error): + withAnimation { + messages.append(Message(type: .info, text: "Failed to select a file: \(error.localizedDescription)")) + } + } + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogManager.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogManager.swift new file mode 100644 index 00000000000..d7c58049a5c --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogManager.swift @@ -0,0 +1,45 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import SwiftUI + +import ExecuTorch + +struct LogEntry: Identifiable, Codable { + let id: UUID + let level: Int + let timestamp: TimeInterval + let filename: String + let line: UInt + let message: String +} + +class LogManager: ObservableObject, LogSink { + @AppStorage("logs") private var data = Data() + + @Published var logs: [LogEntry] = [] { + didSet { + data = (try? JSONEncoder().encode(logs)) ?? Data() + } + } + + init() { + logs = (try? JSONDecoder().decode([LogEntry].self, from: data)) ?? [] + Log.shared.add(sink: self) + } + + deinit { + Log.shared.remove(sink: self) + } + + func log(level: LogLevel, timestamp: TimeInterval, filename: String, line: UInt, message: String) { + let log = LogEntry(id: UUID(), level: level.rawValue, timestamp: timestamp, filename: filename, line: line, message: message) + + DispatchQueue.main.sync { + self.logs.append(log) + } + } + + func clear() { + logs.removeAll() + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogView.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogView.swift new file mode 100644 index 00000000000..bef0c89eddb --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogView.swift @@ -0,0 +1,58 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import SwiftUI + +import ExecuTorch + +struct LogView: View { + @ObservedObject var logManager: LogManager + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + ForEach(logManager.logs) { log in + Text("\(format(timestamp: log.timestamp)) \(log.filename):\(log.line)") + .padding(.top) + .foregroundColor(.secondary) + .textSelection(.enabled) + Text(log.message) + .padding(.bottom) + .foregroundColor(color(for: log.level)) + .textSelection(.enabled) + } + } + } + .padding() + .defaultScrollAnchor(.bottom) + .navigationBarTitle("Logs", displayMode: .inline) + .navigationBarItems(trailing: + Button(action: { logManager.clear() }) { + Image(systemName: "trash") + } + ) + } + + private func format(timestamp: TimeInterval) -> String { + let totalSeconds = Int(timestamp) + let hours = (totalSeconds / 3600) % 24 + let minutes = (totalSeconds / 60) % 60 + let seconds = totalSeconds % 60 + let microseconds = Int((timestamp - Double(totalSeconds)) * 1000000) + return String(format: "%02d:%02d:%02d.%06d", hours, minutes, seconds, microseconds) + } + + private func color(for level: Int) -> Color { + switch LogLevel(rawValue: level) { + case .debug: + return .blue + case .info: + return .primary + case .error: + return .red + case .fatal: + return .purple + default: + return .secondary + } + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/Message.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/Message.swift new file mode 100644 index 00000000000..1c6cdac05cb --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/Message.swift @@ -0,0 +1,18 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import Foundation + +enum MessageType { + case prompted + case generated + case info +} + +struct Message: Identifiable, Equatable { + let id = UUID() + let dateCreated = Date() + var dateUpdated = Date() + var type: MessageType = .prompted + var text = "" + var tokenCount = 0 +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageListView.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageListView.swift new file mode 100644 index 00000000000..7b6da0d73f9 --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageListView.swift @@ -0,0 +1,82 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import SwiftUI + +struct MessageListView: View { + @Binding var messages: [Message] + @State private var showScrollToBottomButton = false + @State private var userHasScrolled = false + @State private var keyboardHeight: CGFloat = 0 + + var body: some View { + ScrollViewReader { value in + ScrollView { + VStack { + ForEach(messages) { message in + MessageView(message: message) + .padding([.leading, .trailing], 20) + } + GeometryReader { geometry -> Color in + DispatchQueue.main.async { + let maxY = geometry.frame(in: .global).maxY + let screenHeight = UIScreen.main.bounds.height - keyboardHeight + let isBeyondBounds = maxY > screenHeight - 50 + if showScrollToBottomButton != isBeyondBounds { + showScrollToBottomButton = isBeyondBounds + userHasScrolled = isBeyondBounds + } + } + return Color.clear + } + .frame(height: 0) + } + } + .onChange(of: messages) { + if !userHasScrolled, let lastMessageId = messages.last?.id { + withAnimation { + value.scrollTo(lastMessageId, anchor: .bottom) + } + } + } + .overlay( + Group { + if showScrollToBottomButton { + Button(action: { + withAnimation { + if let lastMessageId = messages.last?.id { + value.scrollTo(lastMessageId, anchor: .bottom) + } + userHasScrolled = false + } + }) { + ZStack { + Circle() + .fill(Color(UIColor.secondarySystemBackground).opacity(0.9)) + .frame(height: 28) + Image(systemName: "arrow.down.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 28) + } + } + .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) + } + }, + alignment: .bottom + ) + } + .onAppear { + NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in + let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero + keyboardHeight = keyboardFrame.height - 40 + } + NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in + keyboardHeight = 0 + } + } + .onDisappear { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageView.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageView.swift new file mode 100644 index 00000000000..490e7c9be3a --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageView.swift @@ -0,0 +1,54 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import SwiftUI + +struct MessageView: View { + let message: Message + + var body: some View { + VStack(alignment: .center) { + if message.type == .info { + Text(message.text) + .font(.caption) + .foregroundColor(.secondary) + .padding([.leading, .trailing], 10) + } else { + VStack(alignment: message.type == .generated ? .leading : .trailing) { + Text(message.type == .generated ? "LLaMA" : "Prompt") + .font(.caption) + .foregroundColor(.secondary) + .padding(message.type == .generated ? .trailing : .leading, 20) + HStack { + if message.type != .generated { Spacer() } + if message.text.isEmpty { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } else { + Text(message.text) + .padding(10) + .foregroundColor(message.type == .generated ? .primary : .white) + .background(message.type == .generated ? Color(UIColor.secondarySystemBackground) : Color.blue) + .cornerRadius(20) + .contextMenu { + Button(action: { + UIPasteboard.general.string = message.text + }) { + Text("Copy") + Image(systemName: "doc.on.doc") + } + } + } + if message.type == .generated { Spacer() } + } + let elapsedTime = message.dateUpdated.timeIntervalSince(message.dateCreated) + if elapsedTime > 0 && message.type != .info { + Text(String(format: "%.1f t/s", Double(message.tokenCount) / elapsedTime)) + .font(.caption) + .foregroundColor(.secondary) + .padding(message.type == .generated ? .trailing : .leading, 20) + } + }.padding([.leading, .trailing], message.type == .info ? 0 : 10) + } + } + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceManager.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceManager.swift new file mode 100644 index 00000000000..338868a3ff1 --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceManager.swift @@ -0,0 +1,31 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import SwiftUI + +final class ResourceManager: ObservableObject { + @AppStorage("modelPath") var modelPath = "" + @AppStorage("tokenizerPath") var tokenizerPath = "" + private let fileManager = FileManager.default + + var isModelValid: Bool { + fileManager.fileExists(atPath: modelPath) + } + + var isTokenizerValid: Bool { + fileManager.fileExists(atPath: tokenizerPath) + } + + var modelName: String { + URL(fileURLWithPath: modelPath).deletingPathExtension().lastPathComponent + } + + var tokenizerName: String { + URL(fileURLWithPath: tokenizerPath).deletingPathExtension().lastPathComponent + } + + func createDirectoriesIfNeeded() throws { + guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + try fileManager.createDirectory(at: documentsDirectory.appendingPathComponent("models"), withIntermediateDirectories: true, attributes: nil) + try fileManager.createDirectory(at: documentsDirectory.appendingPathComponent("tokenizers"), withIntermediateDirectories: true, attributes: nil) + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceMonitor.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceMonitor.swift new file mode 100644 index 00000000000..5f0a7d6a59a --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceMonitor.swift @@ -0,0 +1,45 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +import Foundation + +final class ResourceMonitor: ObservableObject { + @Published var usedMemory = 0 + @Published var availableMemory = 0 + private var memoryUpdateTimer: Timer? + + deinit { + stop() + } + + public func start() { + memoryUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in + self?.updateMemoryUsage() + } + } + + public func stop() { + memoryUpdateTimer?.invalidate() + } + + private func updateMemoryUsage() { + usedMemory = usedMemoryInMB() + availableMemory = availableMemoryInMB() + } + + private func usedMemoryInMB() -> Int { + var info = mach_task_basic_info() + var count = mach_msg_type_number_t(MemoryLayout.size) / 4 + + let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { + $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { + task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count) + } + } + guard kerr == KERN_SUCCESS else { return 0 } + return Int(info.resident_size / 0x100000) + } + + private func availableMemoryInMB() -> Int { + return Int(os_proc_available_memory() / 0x100000) + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMA/SupportingFiles/LLaMA-Info.plist b/examples/demo-apps/apple_ios/LLaMA/LLaMA/SupportingFiles/LLaMA-Info.plist new file mode 100644 index 00000000000..396d9baa743 --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMA/SupportingFiles/LLaMA-Info.plist @@ -0,0 +1,54 @@ + + + + + FacebookAppID + 323636127310156 + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${FB_BUNDLE_ID} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${FB_BUNDLE_VERSION_SHORT} + CFBundleSignature + ???? + CFBundleVersion + ${FB_BUNDLE_VERSION} + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + + + diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..f4344003c80 --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "logo.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/logo.png b/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/logo.png new file mode 100644 index 00000000000..60e3e5174e9 Binary files /dev/null and b/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/logo.png differ diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/Contents.json b/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Dev.plist b/examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Dev.plist new file mode 100644 index 00000000000..2e779e3e4ee --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Dev.plist @@ -0,0 +1,14 @@ + + + + + get-task-allow + + keychain-access-groups + + T84QZS65DQ.platformFamily + + com.apple.developer.kernel.increased-memory-limit + + + diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Master.plist b/examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Master.plist new file mode 100644 index 00000000000..a4d2cf9818d --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Master.plist @@ -0,0 +1,14 @@ + + + + + get-task-allow + + keychain-access-groups + + 3NW3KR6Q88.platformFamily + + com.apple.developer.kernel.increased-memory-limit + + + diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.h b/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.h new file mode 100644 index 00000000000..95d1ccf5e2e --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.h @@ -0,0 +1,24 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#import + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSErrorDomain const LLaMARunnerErrorDomain; + +NS_SWIFT_NAME(Runner) +@interface LLaMARunner : NSObject + +- (instancetype)initWithModelPath:(NSString *)filePath + tokenizerPath:(NSString *)tokenizerPath; +- (BOOL)isloaded; +- (BOOL)loadWithError:(NSError **)error; +- (BOOL)generate:(NSString *)prompt sequenceLength:(NSInteger)seq_len withTokenCallback:(nullable void (^)(NSString *))callback error:(NSError **)error; +- (void)stop; + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.mm b/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.mm new file mode 100644 index 00000000000..55e71dde35b --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.mm @@ -0,0 +1,102 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#import "LLaMARunner.h" + +#import +#import + +using namespace ::torch::executor; + +NSErrorDomain const LLaMARunnerErrorDomain = @"LLaMARunnerErrorDomain"; + +@interface LLaMARunner () +@end + +@implementation LLaMARunner +{ + std::unique_ptr _runner; +} + +- (instancetype)initWithModelPath:(NSString *)modelPath + tokenizerPath:(NSString *)tokenizerPath +{ + self = [super init]; + if (self) { + [ExecuTorchLog.sharedLog addSink:self]; + _runner = std::make_unique(modelPath.UTF8String, tokenizerPath.UTF8String); + } + return self; +} + +- (void)dealloc +{ + [ExecuTorchLog.sharedLog removeSink:self]; +} + +- (BOOL)isloaded +{ + return _runner->is_loaded(); +} + +- (BOOL)loadWithError:(NSError **)error +{ + const auto status = _runner->load(); + if (status != Error::Ok) { + if (error) { + *error = [NSError errorWithDomain:LLaMARunnerErrorDomain code:(NSInteger)status userInfo:nil]; + } + return NO; + } + return YES; +} + +- (BOOL)generate:(NSString *)prompt sequenceLength:(NSInteger)seq_len withTokenCallback:(nullable void (^)(NSString *))callback error:(NSError **)error +{ + const auto status = _runner->generate( + prompt.UTF8String, + seq_len, + [callback](const std::string & token) { + callback(@(token.c_str())); + } + ); + if (status != Error::Ok) { + if (error) { + *error = [NSError errorWithDomain:LLaMARunnerErrorDomain code:(NSInteger)status userInfo:nil]; + return NO; + } + } + return YES; +} + +- (void)stop +{ + _runner->stop(); +} + +#pragma mark - ExecuTorchLogSink + +- (void)logWithLevel:(ExecuTorchLogLevel)level + timestamp:(NSTimeInterval)timestamp + filename:(NSString *)filename + line:(NSUInteger)line + message:(NSString *)message +{ + NSUInteger totalSeconds = (NSUInteger)timestamp; + NSUInteger hours = (totalSeconds / 3600) % 24; + NSUInteger minutes = (totalSeconds / 60) % 60; + NSUInteger seconds = totalSeconds % 60; + NSUInteger microseconds = (timestamp - totalSeconds) * 1000000; + NSLog( + @"%c %02lu:%02lu:%02lu.%06lu executorch:%s:%zu] %s", + (char)level, + hours, + minutes, + seconds, + microseconds, + filename.UTF8String, + line, + message.UTF8String + ); +} + +@end diff --git a/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/__tests__/RunnerTest.swift b/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/__tests__/RunnerTest.swift new file mode 100644 index 00000000000..a19df8720b7 --- /dev/null +++ b/examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/__tests__/RunnerTest.swift @@ -0,0 +1,28 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +@testable import LLaMARunner + +import XCTest + +final class RunnerTest: XCTestCase { + + func test() { + let bundle = Bundle(for: type(of: self)) + guard let modelPath = bundle.path(forResource: "xnnpack_dq_llama2", ofType: "pte"), + let tokenizerPath = bundle.path(forResource: "flores200sacrebleuspm", ofType: "bin") else { + XCTFail("Couldn't find model or tokenizer files") + return + } + let runner = Runner(modelPath: modelPath, tokenizerPath: tokenizerPath) + var text = "" + + do { + try runner.generate("fr hello", sequenceLength: 128) { token in + text += token + } + } catch { + XCTFail("Failed to generate text with error \(error)") + } + XCTAssertTrue(["bonjour", "salut", "coucou"].map { $0.lowercased() }.contains { text.lowercased().contains($0) }) + } +}