Skip to content

Commit 3bf0abb

Browse files
kirklandsignmalfet
authored andcommitted
Minimal android app build (#491)
* Minimal android app build * Improve script * Detect physical device as well
1 parent ed4ff0b commit 3bf0abb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1274
-29
lines changed

android/Torchchat/.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/caches
5+
/.idea/libraries
6+
/.idea/modules.xml
7+
/.idea/workspace.xml
8+
/.idea/navEditor.xml
9+
/.idea/assetWizardSettings.xml
10+
.DS_Store
11+
/build
12+
/captures
13+
.externalNativeBuild
14+
.cxx
15+
local.properties
16+
*.aar

android/Torchchat/.idea/.gitignore

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

android/Torchchat/app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
id("com.android.application")
3+
}
4+
5+
android {
6+
namespace = "org.pytorch.torchchat"
7+
compileSdk = 33
8+
9+
defaultConfig {
10+
applicationId = "org.pytorch.torchchat"
11+
minSdk = 24
12+
targetSdk = 33
13+
versionCode = 1
14+
versionName = "1.0"
15+
16+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17+
}
18+
19+
buildTypes {
20+
release {
21+
isMinifyEnabled = false
22+
proguardFiles(
23+
getDefaultProguardFile("proguard-android-optimize.txt"),
24+
"proguard-rules.pro"
25+
)
26+
}
27+
}
28+
compileOptions {
29+
sourceCompatibility = JavaVersion.VERSION_1_8
30+
targetCompatibility = JavaVersion.VERSION_1_8
31+
}
32+
buildFeatures {
33+
viewBinding = true
34+
}
35+
}
36+
37+
dependencies {
38+
implementation("androidx.appcompat:appcompat:1.6.1")
39+
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
40+
implementation("com.facebook.fbjni:fbjni:0.5.1")
41+
implementation(files("libs/executorch.aar"))
42+
testImplementation("junit:junit:4.13.2")
43+
androidTestImplementation("androidx.test.ext:junit:1.1.5")
44+
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
45+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.pytorch.torchchat;
2+
3+
import android.content.Context;
4+
5+
import androidx.test.platform.app.InstrumentationRegistry;
6+
import androidx.test.ext.junit.runners.AndroidJUnit4;
7+
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
11+
import static org.junit.Assert.*;
12+
13+
/**
14+
* Instrumented test, which will execute on an Android device.
15+
*
16+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
17+
*/
18+
@RunWith(AndroidJUnit4.class)
19+
public class ExampleInstrumentedTest {
20+
@Test
21+
public void useAppContext() {
22+
// Context of the app under test.
23+
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24+
assertEquals("org.pytorch.torchchat", appContext.getPackageName());
25+
}
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:dataExtractionRules="@xml/data_extraction_rules"
8+
android:fullBackupContent="@xml/backup_rules"
9+
android:icon="@mipmap/ic_launcher"
10+
android:label="@string/app_name"
11+
android:roundIcon="@mipmap/ic_launcher_round"
12+
android:supportsRtl="true"
13+
android:theme="@style/Theme.Torchchat"
14+
tools:targetApi="31">
15+
<activity
16+
android:name=".MainActivity"
17+
android:exported="true"
18+
android:label="@string/app_name"
19+
android:theme="@style/Theme.Torchchat">
20+
<intent-filter>
21+
<action android:name="android.intent.action.MAIN" />
22+
23+
<category android:name="android.intent.category.LAUNCHER" />
24+
</intent-filter>
25+
</activity>
26+
</application>
27+
28+
</manifest>
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
package org.pytorch.torchchat;
10+
11+
import android.app.Activity;
12+
import android.app.ActivityManager;
13+
import android.app.AlertDialog;
14+
import android.content.Context;
15+
import android.os.Bundle;
16+
import android.widget.Button;
17+
import android.widget.EditText;
18+
import android.widget.ImageButton;
19+
import android.widget.ListView;
20+
import java.io.File;
21+
import org.pytorch.executorch.LlamaCallback;
22+
import org.pytorch.executorch.LlamaModule;
23+
24+
public class MainActivity extends Activity implements Runnable, LlamaCallback {
25+
private EditText mEditTextMessage;
26+
private Button mSendButton;
27+
private ImageButton mModelButton;
28+
private ListView mMessagesView;
29+
private MessageAdapter mMessageAdapter;
30+
private LlamaModule mModule = null;
31+
private Message mResultMessage = null;
32+
33+
private String mModelFilePath = "";
34+
private String mTokenizerFilePath = "";
35+
36+
@Override
37+
public void onResult(String result) {
38+
mResultMessage.appendText(result);
39+
run();
40+
}
41+
42+
@Override
43+
public void onStats(float tps) {
44+
runOnUiThread(
45+
() -> {
46+
if (mResultMessage != null) {
47+
mResultMessage.setTokensPerSecond(tps);
48+
mMessageAdapter.notifyDataSetChanged();
49+
}
50+
});
51+
}
52+
53+
private static String[] listLocalFile(String path, String suffix) {
54+
File directory = new File(path);
55+
if (directory.exists() && directory.isDirectory()) {
56+
File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(suffix));
57+
String[] result = new String[files.length];
58+
for (int i = 0; i < files.length; i++) {
59+
if (files[i].isFile() && files[i].getName().endsWith(suffix)) {
60+
result[i] = files[i].getAbsolutePath();
61+
}
62+
}
63+
return result;
64+
}
65+
return null;
66+
}
67+
68+
private void setLocalModel(String modelPath, String tokenizerPath) {
69+
Message modelLoadingMessage = new Message("Loading model...", false);
70+
runOnUiThread(
71+
() -> {
72+
mSendButton.setEnabled(false);
73+
mMessageAdapter.add(modelLoadingMessage);
74+
mMessageAdapter.notifyDataSetChanged();
75+
});
76+
long runStartTime = System.currentTimeMillis();
77+
mModule = new LlamaModule(modelPath, tokenizerPath, 0.8f);
78+
int loadResult = mModule.load();
79+
if (loadResult != 0) {
80+
AlertDialog.Builder builder = new AlertDialog.Builder(this);
81+
builder.setTitle("Load failed: " + loadResult);
82+
runOnUiThread(
83+
() -> {
84+
AlertDialog alert = builder.create();
85+
alert.show();
86+
});
87+
}
88+
89+
long loadDuration = System.currentTimeMillis() - runStartTime;
90+
String modelInfo =
91+
"Model path: "
92+
+ modelPath
93+
+ "\nTokenizer path: "
94+
+ tokenizerPath
95+
+ "\nModel loaded time: "
96+
+ loadDuration
97+
+ " ms";
98+
Message modelLoadedMessage = new Message(modelInfo, false);
99+
runOnUiThread(
100+
() -> {
101+
mSendButton.setEnabled(true);
102+
mMessageAdapter.remove(modelLoadingMessage);
103+
mMessageAdapter.add(modelLoadedMessage);
104+
mMessageAdapter.notifyDataSetChanged();
105+
});
106+
}
107+
108+
private String memoryInfo() {
109+
final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
110+
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
111+
am.getMemoryInfo(memInfo);
112+
return "Total RAM: "
113+
+ Math.floorDiv(memInfo.totalMem, 1000000)
114+
+ " MB. Available RAM: "
115+
+ Math.floorDiv(memInfo.availMem, 1000000)
116+
+ " MB.";
117+
}
118+
119+
private void modelDialog() {
120+
String[] pteFiles = listLocalFile("/data/local/tmp/llama/", ".pte");
121+
String[] binFiles = listLocalFile("/data/local/tmp/llama/", ".bin");
122+
String[] modelFiles = listLocalFile("/data/local/tmp/llama/", ".model");
123+
String[] tokenizerFiles = new String[binFiles.length + modelFiles.length];
124+
System.arraycopy(binFiles, 0, tokenizerFiles, 0, binFiles.length);
125+
System.arraycopy(modelFiles, 0, tokenizerFiles, binFiles.length, modelFiles.length);
126+
AlertDialog.Builder modelPathBuilder = new AlertDialog.Builder(this);
127+
modelPathBuilder.setTitle("Select model path");
128+
AlertDialog.Builder tokenizerPathBuilder = new AlertDialog.Builder(this);
129+
tokenizerPathBuilder.setTitle("Select tokenizer path");
130+
modelPathBuilder.setSingleChoiceItems(
131+
pteFiles,
132+
-1,
133+
(dialog, item) -> {
134+
mModelFilePath = pteFiles[item];
135+
mEditTextMessage.setText("");
136+
dialog.dismiss();
137+
tokenizerPathBuilder.create().show();
138+
});
139+
140+
tokenizerPathBuilder.setSingleChoiceItems(
141+
tokenizerFiles,
142+
-1,
143+
(dialog, item) -> {
144+
mTokenizerFilePath = tokenizerFiles[item];
145+
Runnable runnable =
146+
new Runnable() {
147+
@Override
148+
public void run() {
149+
setLocalModel(mModelFilePath, mTokenizerFilePath);
150+
}
151+
};
152+
new Thread(runnable).start();
153+
dialog.dismiss();
154+
});
155+
156+
modelPathBuilder.create().show();
157+
}
158+
159+
@Override
160+
protected void onCreate(Bundle savedInstanceState) {
161+
super.onCreate(savedInstanceState);
162+
setContentView(R.layout.activity_main);
163+
164+
mEditTextMessage = findViewById(R.id.editTextMessage);
165+
mSendButton = findViewById(R.id.sendButton);
166+
mSendButton.setEnabled(false);
167+
mModelButton = findViewById(R.id.modelButton);
168+
mMessagesView = findViewById(R.id.messages_view);
169+
mMessageAdapter = new MessageAdapter(this, R.layout.sent_message);
170+
mMessagesView.setAdapter(mMessageAdapter);
171+
mModelButton.setOnClickListener(
172+
view -> {
173+
mModule.stop();
174+
mMessageAdapter.clear();
175+
mMessageAdapter.notifyDataSetChanged();
176+
modelDialog();
177+
});
178+
179+
onModelRunStopped();
180+
modelDialog();
181+
}
182+
183+
private void onModelRunStarted() {
184+
mSendButton.setText("Stop");
185+
mSendButton.setOnClickListener(
186+
view -> {
187+
mModule.stop();
188+
});
189+
}
190+
191+
private void onModelRunStopped() {
192+
setTitle(memoryInfo());
193+
mSendButton.setText("Generate");
194+
mSendButton.setOnClickListener(
195+
view -> {
196+
String prompt = mEditTextMessage.getText().toString();
197+
mMessageAdapter.add(new Message(prompt, true));
198+
mMessageAdapter.notifyDataSetChanged();
199+
mEditTextMessage.setText("");
200+
mResultMessage = new Message("", false);
201+
mMessageAdapter.add(mResultMessage);
202+
Runnable runnable =
203+
new Runnable() {
204+
@Override
205+
public void run() {
206+
runOnUiThread(
207+
new Runnable() {
208+
@Override
209+
public void run() {
210+
onModelRunStarted();
211+
}
212+
});
213+
214+
mModule.generate(prompt, MainActivity.this);
215+
216+
runOnUiThread(
217+
new Runnable() {
218+
@Override
219+
public void run() {
220+
onModelRunStopped();
221+
}
222+
});
223+
}
224+
};
225+
new Thread(runnable).start();
226+
});
227+
mMessageAdapter.notifyDataSetChanged();
228+
}
229+
230+
@Override
231+
public void run() {
232+
runOnUiThread(
233+
new Runnable() {
234+
@Override
235+
public void run() {
236+
mMessageAdapter.notifyDataSetChanged();
237+
setTitle(memoryInfo());
238+
}
239+
});
240+
}
241+
}

0 commit comments

Comments
 (0)