From 923a73c0a1161aca614966b00c1b9750ceed613f Mon Sep 17 00:00:00 2001 From: Anthony Shoumikhin Date: Mon, 11 Mar 2024 15:21:55 -0700 Subject: [PATCH] Move demo app to OSS. Summary: . Reviewed By: kirklandsign Differential Revision: D54769508 --- .../LLaMA/LLaMA/Application/App.swift | 12 + .../LLaMA/LLaMA/Application/ContentView.swift | 273 ++++++++++++++++++ .../LLaMA/LLaMA/Application/LogManager.swift | 45 +++ .../LLaMA/LLaMA/Application/LogView.swift | 58 ++++ .../LLaMA/LLaMA/Application/Message.swift | 18 ++ .../LLaMA/Application/MessageListView.swift | 82 ++++++ .../LLaMA/LLaMA/Application/MessageView.swift | 54 ++++ .../LLaMA/Application/ResourceManager.swift | 31 ++ .../LLaMA/Application/ResourceMonitor.swift | 45 +++ .../LLaMA/SupportingFiles/LLaMA-Info.plist | 54 ++++ .../AppIcon.appiconset/Contents.json | 14 + .../AppIcon.appiconset/logo.png | Bin 0 -> 33036 bytes .../LLaMAAssets/Assets.xcassets/Contents.json | 6 + .../LLaMAEntitlements/Entitlements-Dev.plist | 14 + .../Entitlements-Master.plist | 14 + .../LLaMARunner/Exported/LLaMARunner.h | 24 ++ .../LLaMARunner/Exported/LLaMARunner.mm | 102 +++++++ .../LLaMARunner/__tests__/RunnerTest.swift | 28 ++ 18 files changed, 874 insertions(+) create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/App.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ContentView.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogManager.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/LogView.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/Message.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageListView.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/MessageView.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceManager.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/Application/ResourceMonitor.swift create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMA/SupportingFiles/LLaMA-Info.plist create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/AppIcon.appiconset/logo.png create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMAAssets/Assets.xcassets/Contents.json create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Dev.plist create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMAEntitlements/Entitlements-Master.plist create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.h create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/Exported/LLaMARunner.mm create mode 100644 examples/demo-apps/apple_ios/LLaMA/LLaMARunner/LLaMARunner/__tests__/RunnerTest.swift 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 0000000000000000000000000000000000000000..60e3e5174e9bdec2caf09cd42a9232e1dff65530 GIT binary patch literal 33036 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 + + + + 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) }) + } +}