Skip to content

Commit 8dfb030

Browse files
shoumikhinfacebook-github-bot
authored andcommitted
Move demo app to OSS. (#2355)
Summary: Pull Request resolved: #2355 bypass-github-export-checks Reviewed By: kirklandsign Differential Revision: D54769508 fbshipit-source-id: 219e27746e6e59ecd62506ad1b874492e4c1c8b0
1 parent 02edc9e commit 8dfb030

File tree

18 files changed

+873
-0
lines changed

18 files changed

+873
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
3+
import SwiftUI
4+
5+
@main
6+
struct App: SwiftUI.App {
7+
var body: some Scene {
8+
WindowGroup {
9+
ContentView()
10+
}
11+
}
12+
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
3+
import SwiftUI
4+
import UniformTypeIdentifiers
5+
6+
import LLaMARunner
7+
8+
class RunnerHolder: ObservableObject {
9+
var runner: Runner?
10+
}
11+
12+
struct ContentView: View {
13+
@State private var prompt = ""
14+
@State private var messages: [Message] = []
15+
@State private var showingLogs = false
16+
@State private var pickerType: PickerType?
17+
@State private var isGenerating = false
18+
@State private var shouldStopGenerating = false
19+
private let runnerQueue = DispatchQueue(label: "org.pytorch.executorch.llama")
20+
@StateObject private var runnerHolder = RunnerHolder()
21+
@StateObject private var resourceManager = ResourceManager()
22+
@StateObject private var resourceMonitor = ResourceMonitor()
23+
@StateObject private var logManager = LogManager()
24+
25+
enum PickerType {
26+
case model
27+
case tokenizer
28+
}
29+
30+
private var placeholder: String {
31+
resourceManager.isModelValid ? resourceManager.isTokenizerValid ? "Prompt..." : "Select Tokenizer..." : "Select Model..."
32+
}
33+
34+
private var title: String {
35+
resourceManager.isModelValid ? resourceManager.isTokenizerValid ? resourceManager.modelName : "Select Tokenizer..." : "Select Model..."
36+
}
37+
38+
private var modelTitle: String {
39+
resourceManager.isModelValid ? resourceManager.modelName : "Select Model..."
40+
}
41+
42+
private var tokenizerTitle: String {
43+
resourceManager.isTokenizerValid ? resourceManager.tokenizerName : "Select Tokenizer..."
44+
}
45+
46+
private var isInputEnabled: Bool { resourceManager.isModelValid && resourceManager.isTokenizerValid }
47+
48+
var body: some View {
49+
NavigationView {
50+
VStack {
51+
MessageListView(messages: $messages)
52+
.gesture(
53+
DragGesture().onChanged { value in
54+
if value.translation.height > 10 {
55+
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
56+
}
57+
}
58+
)
59+
HStack {
60+
Menu {
61+
Section(header: Text("Model")) {
62+
Button(action: { pickerType = .model }) {
63+
Label(modelTitle, systemImage: "doc")
64+
}
65+
}
66+
Section(header: Text("Tokenizer")) {
67+
Button(action: { pickerType = .tokenizer }) {
68+
Label(tokenizerTitle, systemImage: "doc")
69+
}
70+
}
71+
} label: {
72+
Image(systemName: "ellipsis.circle")
73+
.resizable()
74+
.aspectRatio(contentMode: .fit)
75+
.frame(height: 28)
76+
}
77+
.disabled(isGenerating)
78+
79+
TextField(placeholder, text: $prompt, axis: .vertical)
80+
.padding(8)
81+
.background(Color.gray.opacity(0.1))
82+
.cornerRadius(20)
83+
.lineLimit(1...10)
84+
.overlay(
85+
RoundedRectangle(cornerRadius: 20)
86+
.stroke(isInputEnabled ? Color.blue : Color.gray, lineWidth: 1)
87+
)
88+
.disabled(!isInputEnabled)
89+
90+
Button(action: isGenerating ? stop : generate) {
91+
Image(systemName: isGenerating ? "stop.circle" : "arrowshape.up.circle.fill")
92+
.resizable()
93+
.aspectRatio(contentMode: .fit)
94+
.frame(height: 28)
95+
}
96+
.disabled(isGenerating ? shouldStopGenerating : (!isInputEnabled || prompt.isEmpty))
97+
}
98+
.padding([.leading, .trailing, .bottom], 10)
99+
}
100+
.navigationBarTitle(title, displayMode: .inline)
101+
.navigationBarItems(trailing:
102+
HStack {
103+
Menu {
104+
Section(header: Text("Memory")) {
105+
Text("Used: \(resourceMonitor.usedMemory) Mb")
106+
Text("Available: \(resourceMonitor.availableMemory) Mb")
107+
}
108+
} label: {
109+
Text("\(resourceMonitor.usedMemory) Mb")
110+
}
111+
.onAppear {
112+
resourceMonitor.start()
113+
}
114+
.onDisappear {
115+
resourceMonitor.stop()
116+
}
117+
Button(action: { showingLogs = true }) {
118+
Image(systemName: "list.bullet.rectangle")
119+
}
120+
}
121+
)
122+
.sheet(isPresented: $showingLogs) {
123+
NavigationView {
124+
LogView(logManager: logManager)
125+
}
126+
}
127+
.fileImporter(
128+
isPresented: Binding<Bool>(
129+
get: { pickerType != nil },
130+
set: { if !$0 { pickerType = nil } }
131+
),
132+
allowedContentTypes: allowedContentTypes(),
133+
allowsMultipleSelection: false
134+
) { [pickerType] result in
135+
handleFileImportResult(pickerType, result)
136+
}
137+
.onAppear {
138+
do {
139+
try resourceManager.createDirectoriesIfNeeded()
140+
} catch {
141+
withAnimation {
142+
messages.append(Message(type: .info, text: "Error creating content directories: \(error.localizedDescription)"))
143+
}
144+
}
145+
}
146+
}
147+
}
148+
149+
private func generate() {
150+
guard !prompt.isEmpty else { return }
151+
isGenerating = true
152+
shouldStopGenerating = false
153+
let text = prompt.trimmingCharacters(in: .whitespacesAndNewlines)
154+
let seq_len = 128
155+
prompt = ""
156+
let modelPath = resourceManager.modelPath
157+
let tokenizerPath = resourceManager.tokenizerPath
158+
159+
messages.append(Message(text: text))
160+
messages.append(Message(type: .generated))
161+
162+
runnerQueue.async {
163+
defer {
164+
DispatchQueue.main.async {
165+
isGenerating = false
166+
}
167+
}
168+
runnerHolder.runner = runnerHolder.runner ?? Runner(modelPath: modelPath, tokenizerPath: tokenizerPath)
169+
guard !shouldStopGenerating else { return }
170+
if let runner = runnerHolder.runner, !runner.isloaded() {
171+
var error: Error?
172+
let startLoadTime = Date()
173+
do {
174+
try runner.load()
175+
} catch let loadError {
176+
error = loadError
177+
}
178+
let loadTime = Date().timeIntervalSince(startLoadTime)
179+
DispatchQueue.main.async {
180+
withAnimation {
181+
var message = messages.removeLast()
182+
message.type = .info
183+
if let error {
184+
message.text = "Model loading failed: error \((error as NSError).code)"
185+
} else {
186+
message.text = "Model loaded in \(String(format: "%.1f", loadTime)) s"
187+
}
188+
messages.append(message)
189+
if error == nil {
190+
messages.append(Message(type: .generated))
191+
}
192+
}
193+
}
194+
if error != nil {
195+
return
196+
}
197+
}
198+
guard !shouldStopGenerating else {
199+
DispatchQueue.main.async {
200+
withAnimation {
201+
_ = messages.removeLast()
202+
}
203+
}
204+
return
205+
}
206+
do {
207+
try runnerHolder.runner?.generate(text, sequenceLength: seq_len) { token in
208+
209+
DispatchQueue.main.async {
210+
withAnimation {
211+
var message = messages.removeLast()
212+
message.text += token
213+
message.tokenCount += 1
214+
message.dateUpdated = Date()
215+
messages.append(message)
216+
}
217+
}
218+
if shouldStopGenerating {
219+
runnerHolder.runner?.stop()
220+
}
221+
}
222+
} catch {
223+
DispatchQueue.main.async {
224+
withAnimation {
225+
var message = messages.removeLast()
226+
message.type = .info
227+
message.text = "Text generation failed: error \((error as NSError).code)"
228+
messages.append(message)
229+
}
230+
}
231+
}
232+
}
233+
}
234+
235+
private func stop() {
236+
shouldStopGenerating = true
237+
}
238+
239+
private func allowedContentTypes() -> [UTType] {
240+
guard let pickerType else { return [] }
241+
switch pickerType {
242+
case .model:
243+
return [UTType(filenameExtension: "pte")].compactMap { $0 }
244+
case .tokenizer:
245+
return [UTType(filenameExtension: "bin")].compactMap { $0 }
246+
}
247+
}
248+
249+
private func handleFileImportResult(_ pickerType: PickerType?, _ result: Result<[URL], Error>) {
250+
switch result {
251+
case .success(let urls):
252+
guard let url = urls.first, let pickerType else {
253+
withAnimation {
254+
messages.append(Message(type: .info, text: "Failed to select a file"))
255+
}
256+
return
257+
}
258+
runnerQueue.async {
259+
runnerHolder.runner = nil
260+
}
261+
switch pickerType {
262+
case .model:
263+
resourceManager.modelPath = url.path
264+
case .tokenizer:
265+
resourceManager.tokenizerPath = url.path
266+
}
267+
case .failure(let error):
268+
withAnimation {
269+
messages.append(Message(type: .info, text: "Failed to select a file: \(error.localizedDescription)"))
270+
}
271+
}
272+
}
273+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
3+
import SwiftUI
4+
5+
import ExecuTorch
6+
7+
struct LogEntry: Identifiable, Codable {
8+
let id: UUID
9+
let level: Int
10+
let timestamp: TimeInterval
11+
let filename: String
12+
let line: UInt
13+
let message: String
14+
}
15+
16+
class LogManager: ObservableObject, LogSink {
17+
@AppStorage("logs") private var data = Data()
18+
19+
@Published var logs: [LogEntry] = [] {
20+
didSet {
21+
data = (try? JSONEncoder().encode(logs)) ?? Data()
22+
}
23+
}
24+
25+
init() {
26+
logs = (try? JSONDecoder().decode([LogEntry].self, from: data)) ?? []
27+
Log.shared.add(sink: self)
28+
}
29+
30+
deinit {
31+
Log.shared.remove(sink: self)
32+
}
33+
34+
func log(level: LogLevel, timestamp: TimeInterval, filename: String, line: UInt, message: String) {
35+
let log = LogEntry(id: UUID(), level: level.rawValue, timestamp: timestamp, filename: filename, line: line, message: message)
36+
37+
DispatchQueue.main.sync {
38+
self.logs.append(log)
39+
}
40+
}
41+
42+
func clear() {
43+
logs.removeAll()
44+
}
45+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
3+
import SwiftUI
4+
5+
import ExecuTorch
6+
7+
struct LogView: View {
8+
@ObservedObject var logManager: LogManager
9+
10+
var body: some View {
11+
ScrollView {
12+
VStack(alignment: .leading) {
13+
ForEach(logManager.logs) { log in
14+
Text("\(format(timestamp: log.timestamp)) \(log.filename):\(log.line)")
15+
.padding(.top)
16+
.foregroundColor(.secondary)
17+
.textSelection(.enabled)
18+
Text(log.message)
19+
.padding(.bottom)
20+
.foregroundColor(color(for: log.level))
21+
.textSelection(.enabled)
22+
}
23+
}
24+
}
25+
.padding()
26+
.defaultScrollAnchor(.bottom)
27+
.navigationBarTitle("Logs", displayMode: .inline)
28+
.navigationBarItems(trailing:
29+
Button(action: { logManager.clear() }) {
30+
Image(systemName: "trash")
31+
}
32+
)
33+
}
34+
35+
private func format(timestamp: TimeInterval) -> String {
36+
let totalSeconds = Int(timestamp)
37+
let hours = (totalSeconds / 3600) % 24
38+
let minutes = (totalSeconds / 60) % 60
39+
let seconds = totalSeconds % 60
40+
let microseconds = Int((timestamp - Double(totalSeconds)) * 1000000)
41+
return String(format: "%02d:%02d:%02d.%06d", hours, minutes, seconds, microseconds)
42+
}
43+
44+
private func color(for level: Int) -> Color {
45+
switch LogLevel(rawValue: level) {
46+
case .debug:
47+
return .blue
48+
case .info:
49+
return .primary
50+
case .error:
51+
return .red
52+
case .fatal:
53+
return .purple
54+
default:
55+
return .secondary
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)