Skip to content

Commit 463a5e5

Browse files
omkar-334psiddh
andauthored
docs: add Kotlin examples and fix MULTIMODAL alias note for Android LLM runner (#20147)
### Summary Follow up to #8790, #19611 (comment) 1. Added Kotlin snippets next to every Java block in `docs/source/llm/run-on-android.md`. Same shape as `run-on-ios.md`, with a `Language:` label paragraph above each fenced block (no tab-set). Covers imports, the simple `LlmModule` constructor, the `LlmModuleConfig` builder, `load`, the `LlmCallback` object expression, `LlmGenerationConfig`, `stop`, `resetContext`, image prefill (`IntArray` / `ByteBuffer` / `FloatArray`), `prefillNormalizedImage`, `prefillAudio`, `prefillRawAudio`, and the multimodal `generate` overload. Uses idiomatic Kotlin (`object : LlmCallback`, `IntArray`/`FloatArray`, `Float.SIZE_BYTES`, `ByteBuffer.apply { ... }`, trailing commas). 2. Reworded the available-model-types sentence so it no longer lists `MODEL_TYPE_MULTIMODAL` as a separate type. Both constants share the value `2` in `LlmModuleConfig.java:53,56`, so listing them implied behavior that does not exist. The new sentence notes the alias explicitly. cc @mergennachin @nil-is-all @kirklandsign @cbilgin @larryliu0820 @cccclai @helunwencser @jackzhxng @digantdesai @byjlw --------- Co-authored-by: Siddartha Pothapragada <sidart@meta.com>
1 parent d8e8d1e commit 463a5e5

1 file changed

Lines changed: 170 additions & 8 deletions

File tree

docs/source/llm/run-on-android.md

Lines changed: 170 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Running LLMs on Android
22

3-
ExecuTorch's LLM-specific runtime components provide an experimental Java interface around the core C++ LLM runtime, available through the `executorch-android` AAR.
3+
ExecuTorch's LLM-specific runtime components provide experimental Java APIs, callable from Java or Kotlin, around the core C++ LLM runtime. These APIs are available through the `executorch-android` AAR.
44

55
## Prerequisites
66

@@ -10,36 +10,64 @@ To add the `executorch-android` library to your app, see [Using ExecuTorch on An
1010

1111
## Runtime API
1212

13-
Once the `executorch-android` AAR is on your classpath, you can import the LLM runner classes from the `org.pytorch.executorch.extension.llm` package.
13+
Once the `executorch-android` AAR is on your classpath, you can import the LLM runner classes from the `org.pytorch.executorch.extension.llm` package. The runner is callable from both Java and Kotlin; the rest of this guide includes both Java and Kotlin examples for each snippet.
1414

1515
### Importing
1616

17+
Java:
1718
```java
1819
import org.pytorch.executorch.extension.llm.LlmModule;
1920
import org.pytorch.executorch.extension.llm.LlmModuleConfig;
2021
import org.pytorch.executorch.extension.llm.LlmGenerationConfig;
2122
import org.pytorch.executorch.extension.llm.LlmCallback;
23+
24+
// Only needed for the multimodal ByteBuffer paths in the Images section.
25+
import java.nio.ByteBuffer;
26+
import java.nio.ByteOrder;
27+
```
28+
29+
Kotlin:
30+
```kotlin
31+
import org.pytorch.executorch.extension.llm.LlmModule
32+
import org.pytorch.executorch.extension.llm.LlmModuleConfig
33+
import org.pytorch.executorch.extension.llm.LlmGenerationConfig
34+
import org.pytorch.executorch.extension.llm.LlmCallback
35+
36+
// Only needed for the multimodal ByteBuffer paths in the Images section.
37+
import java.nio.ByteBuffer
38+
import java.nio.ByteOrder
2239
```
2340

2441
### LlmModule
2542

26-
The `LlmModule` class provides a simple Java interface for loading a text-generation model, configuring its tokenizer, generating token streams, and stopping execution. It also supports multimodal models that accept image and audio inputs alongside a text prompt.
43+
The `LlmModule` class provides a simple interface, usable from Java and Kotlin, for loading a text-generation model, configuring its tokenizer, generating token streams, and stopping execution. It also supports multimodal models that accept image and audio inputs alongside a text prompt.
2744

2845
This API is experimental and subject to change.
2946

3047
#### Initialization
3148

3249
Create an `LlmModule` by specifying paths to your serialized model (`.pte`) and tokenizer files. For text-only models, the simple constructor is enough:
3350

51+
Java:
3452
```java
3553
LlmModule module = new LlmModule(
3654
"/data/local/tmp/llama-3.2-instruct.pte",
3755
"/data/local/tmp/tokenizer.model",
3856
0.8f);
3957
```
4058

59+
Kotlin:
60+
```kotlin
61+
val module = LlmModule(
62+
"/data/local/tmp/llama-3.2-instruct.pte",
63+
"/data/local/tmp/tokenizer.model",
64+
0.8f
65+
)
66+
```
67+
4168
For finer control (multimodal model type, BOS/EOS handling, supplementary data files, load mode), use `LlmModuleConfig` with the fluent builder:
4269

70+
Java:
4371
```java
4472
LlmModuleConfig config = LlmModuleConfig.create()
4573
.modulePath("/data/local/tmp/llama-3.2-instruct.pte")
@@ -52,27 +80,50 @@ LlmModuleConfig config = LlmModuleConfig.create()
5280
LlmModule module = new LlmModule(config);
5381
```
5482

55-
Available load modes are `LOAD_MODE_FILE`, `LOAD_MODE_MMAP` (default), `LOAD_MODE_MMAP_USE_MLOCK`, and `LOAD_MODE_MMAP_USE_MLOCK_IGNORE_ERRORS`. Available model types are `MODEL_TYPE_TEXT`, `MODEL_TYPE_TEXT_VISION`, and `MODEL_TYPE_MULTIMODAL`.
83+
Kotlin:
84+
```kotlin
85+
val config = LlmModuleConfig.create()
86+
.modulePath("/data/local/tmp/llama-3.2-instruct.pte")
87+
.tokenizerPath("/data/local/tmp/tokenizer.model")
88+
.temperature(0.8f)
89+
.modelType(LlmModuleConfig.MODEL_TYPE_TEXT)
90+
.loadMode(LlmModuleConfig.LOAD_MODE_MMAP)
91+
.build()
92+
93+
val module = LlmModule(config)
94+
```
95+
96+
Available load modes are `LOAD_MODE_FILE`, `LOAD_MODE_MMAP` (default), `LOAD_MODE_MMAP_USE_MLOCK`, and `LOAD_MODE_MMAP_USE_MLOCK_IGNORE_ERRORS`. Available model types are `MODEL_TYPE_TEXT` and `MODEL_TYPE_TEXT_VISION` (the `MODEL_TYPE_MULTIMODAL` constant is currently an alias for `MODEL_TYPE_TEXT_VISION` and selects the same runtime path).
5697

5798
Construction itself is lightweight and does not load the program data immediately.
5899

59100
#### Loading
60101

61102
Explicitly load the model before generation to avoid paying the load cost during your first `generate` call.
62103

104+
Java:
63105
```java
64106
int status = module.load();
65107
if (status != 0) {
66108
// Handle load failure (status is an ExecuTorch runtime error code).
67109
}
68110
```
69111

112+
Kotlin:
113+
```kotlin
114+
val status = module.load()
115+
if (status != 0) {
116+
// Handle load failure (status is an ExecuTorch runtime error code).
117+
}
118+
```
119+
70120
If you skip this step, the model is loaded lazily on the first `generate` call.
71121

72122
#### Generating
73123

74124
Generate tokens from a text prompt by passing an `LlmCallback` that receives each token as it is produced. The same callback also receives a JSON-encoded statistics string when generation completes.
75125

126+
Java:
76127
```java
77128
LlmCallback callback = new LlmCallback() {
78129
@Override
@@ -97,8 +148,31 @@ LlmCallback callback = new LlmCallback() {
97148
module.generate("Once upon a time", callback);
98149
```
99150

151+
Kotlin:
152+
```kotlin
153+
val callback = object : LlmCallback {
154+
override fun onResult(token: String) {
155+
// Called once per generated token. Append to your UI buffer here.
156+
print(token)
157+
}
158+
159+
override fun onStats(statsJson: String) {
160+
// Called once when generation finishes. See extension/llm/runner/stats.h
161+
// for the field definitions.
162+
println("\n$statsJson")
163+
}
164+
165+
override fun onError(errorCode: Int, message: String) {
166+
// Called if the runtime reports an error during generation.
167+
}
168+
}
169+
170+
module.generate("Once upon a time", callback)
171+
```
172+
100173
For full control over generation parameters, use `LlmGenerationConfig`:
101174

175+
Java:
102176
```java
103177
LlmGenerationConfig genConfig = LlmGenerationConfig.create()
104178
.seqLen(2048)
@@ -109,85 +183,162 @@ LlmGenerationConfig genConfig = LlmGenerationConfig.create()
109183
module.generate("Once upon a time", genConfig, callback);
110184
```
111185

186+
Kotlin:
187+
```kotlin
188+
val genConfig = LlmGenerationConfig.create()
189+
.seqLen(2048)
190+
.temperature(0.8f)
191+
.echo(false)
192+
.build()
193+
194+
module.generate("Once upon a time", genConfig, callback)
195+
```
196+
112197
`LlmGenerationConfig` exposes `echo`, `maxNewTokens`, `seqLen`, `temperature`, `numBos`, `numEos`, and `warming`. Defaults match the C++ `GenerationConfig` documented in [Running LLMs with C++](run-with-c-plus-plus.md).
113198

114199
#### Stopping Generation
115200

116201
If you need to interrupt a long-running generation, call `stop()` from another thread (or from inside the `onResult` callback):
117202

203+
Java:
118204
```java
119205
module.stop();
120206
```
121207

208+
Kotlin:
209+
```kotlin
210+
module.stop()
211+
```
212+
122213
Generation also runs synchronously on the calling thread, so make sure you invoke `generate()` off the main thread (for example, on a `HandlerThread` or via a `java.util.concurrent.Executor`).
123214

124215
#### Resetting
125216

126217
To clear the prefilled tokens from the KV cache and reset the start position to 0, call:
127218

219+
Java:
128220
```java
129221
module.resetContext();
130222
```
131223

224+
Kotlin:
225+
```kotlin
226+
module.resetContext()
227+
```
228+
132229
This is the equivalent of `reset()` on the iOS runner and `reset()` on the C++ `IRunner`.
133230

134231
### Multimodal Inputs
135232

136-
For models declared as `MODEL_TYPE_TEXT_VISION` or `MODEL_TYPE_MULTIMODAL`, image and audio data are provided through dedicated prefill methods. After prefilling all modalities, call `generate()` with the text prompt to produce the response.
233+
For models declared as `MODEL_TYPE_TEXT_VISION` (`MODEL_TYPE_MULTIMODAL` is currently an alias), image and audio data are provided through dedicated prefill methods. After prefilling all modalities, call `generate()` with the text prompt to produce the response.
137234

138235
#### Images
139236

140237
Raw uint8 pixel data in CHW order can be supplied as an `int[]`, or as a direct `ByteBuffer` to avoid JNI array copies:
141238

239+
Java:
142240
```java
143241
// As int[]
144242
int[] pixels = ...; // length == channels * height * width
145243
module.prefillImages(pixels, /*width=*/336, /*height=*/336, /*channels=*/3);
146244

147245
// As direct ByteBuffer (preferred for large images)
246+
byte[] rawBytes = ...; // length == channels * height * width
148247
ByteBuffer buffer = ByteBuffer.allocateDirect(3 * 336 * 336);
149-
buffer.put(rawBytes).rewind();
248+
buffer.put(rawBytes);
249+
// Rewind so the JNI side reads from position 0.
250+
buffer.rewind();
150251
module.prefillImages(buffer, 336, 336, 3);
151252
```
152253

153-
Pre-normalized float pixel data is also supported, both as a `float[]` and as a direct `ByteBuffer` in native byte order:
254+
Kotlin:
255+
```kotlin
256+
// As IntArray
257+
val pixels: IntArray = ... // length == channels * height * width
258+
module.prefillImages(pixels, /* width = */ 336, /* height = */ 336, /* channels = */ 3)
259+
260+
// As direct ByteBuffer (preferred for large images)
261+
val rawBytes: ByteArray = ... // length == channels * height * width
262+
val buffer = ByteBuffer.allocateDirect(3 * 336 * 336).apply {
263+
put(rawBytes)
264+
rewind()
265+
}
266+
module.prefillImages(buffer, 336, 336, 3)
267+
```
268+
269+
Pre-normalized float pixel data is also supported, both as a `float[]` and as a direct `ByteBuffer` in native byte order. The two paths intentionally hit different methods: the `float[]` overload is `prefillImages`, while the `ByteBuffer` path is `prefillNormalizedImage` (the names reflect the underlying JNI bindings and are not interchangeable).
154270

271+
Java:
155272
```java
156273
float[] normalized = ...; // length == channels * height * width
157274
module.prefillImages(normalized, 336, 336, 3);
158275

159276
ByteBuffer floatBuffer = ByteBuffer
160277
.allocateDirect(3 * 336 * 336 * Float.BYTES)
161278
.order(ByteOrder.nativeOrder());
162-
// fill floatBuffer with normalized values, then:
279+
// fill floatBuffer with normalized values, then rewind before the call:
280+
floatBuffer.rewind();
163281
module.prefillNormalizedImage(floatBuffer, 336, 336, 3);
164282
```
165283

284+
Kotlin:
285+
```kotlin
286+
val normalized: FloatArray = ... // length == channels * height * width
287+
module.prefillImages(normalized, 336, 336, 3)
288+
289+
val floatBuffer: ByteBuffer = ByteBuffer
290+
.allocateDirect(3 * 336 * 336 * Float.SIZE_BYTES)
291+
.order(ByteOrder.nativeOrder())
292+
// fill floatBuffer with normalized values, then rewind before the call:
293+
floatBuffer.rewind()
294+
module.prefillNormalizedImage(floatBuffer, 336, 336, 3)
295+
```
296+
166297
#### Audio
167298

168299
Preprocessed audio features (for example mel spectrograms produced by a Whisper preprocessor) can be supplied as `byte[]` or `float[]`:
169300

301+
Java:
170302
```java
171303
module.prefillAudio(features, /*batchSize=*/1, /*nBins=*/128, /*nFrames=*/3000);
172304
```
173305

306+
Kotlin:
307+
```kotlin
308+
module.prefillAudio(features, /* batchSize = */ 1, /* nBins = */ 128, /* nFrames = */ 3000)
309+
```
310+
174311
Raw audio samples can be supplied with `prefillRawAudio`:
175312

313+
Java:
176314
```java
177315
module.prefillRawAudio(samples, /*batchSize=*/1, /*nChannels=*/1, /*nSamples=*/16000);
178316
```
179317

318+
Kotlin:
319+
```kotlin
320+
module.prefillRawAudio(samples, /* batchSize = */ 1, /* nChannels = */ 1, /* nSamples = */ 16000)
321+
```
322+
180323
#### Generating with Multimodal Prefill
181324

182325
After prefilling each modality, run `generate()` with the text prompt as usual:
183326

327+
Java:
184328
```java
185329
module.prefillImages(pixels, 336, 336, 3);
186330
module.generate("What's in this image?", callback);
187331
```
188332

333+
Kotlin:
334+
```kotlin
335+
module.prefillImages(pixels, 336, 336, 3)
336+
module.generate("What's in this image?", callback)
337+
```
338+
189339
For text-vision models, a convenience overload accepts the image and prompt together:
190340

341+
Java:
191342
```java
192343
module.generate(
193344
pixels, /*width=*/336, /*height=*/336, /*channels=*/3,
@@ -197,6 +348,17 @@ module.generate(
197348
/*echo=*/false);
198349
```
199350

351+
Kotlin:
352+
```kotlin
353+
module.generate(
354+
pixels, /* width = */ 336, /* height = */ 336, /* channels = */ 3,
355+
"What's in this image?",
356+
/* seqLen = */ 768,
357+
callback,
358+
/* echo = */ false
359+
)
360+
```
361+
200362
## Demo
201363

202364
See the [Llama Android demo app](https://github.com/meta-pytorch/executorch-examples/tree/main/llm/android/LlamaDemo) in `executorch-examples` for an end-to-end project that wires `LlmModule`, `LlmCallback`, and a `HandlerThread` into a chat UI.

0 commit comments

Comments
 (0)