Skip to content

Commit 29c0525

Browse files
committed
feat: Add Kotlin API layer with coroutine support
1 parent e62e2f2 commit 29c0525

11 files changed

Lines changed: 1051 additions & 53 deletions

File tree

.gitignore

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
*.iml
22
.gradle
33
/local.properties
4-
/.idea/caches
5-
/.idea/libraries
6-
/.idea/modules.xml
7-
/.idea/workspace.xml
8-
/.idea/navEditor.xml
9-
/.idea/assetWizardSettings.xml
4+
.idea
105
.DS_Store
116
/build
127
/captures

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "app/src/main/cpp/llama.cpp"]
2+
path = app/src/main/cpp/llama.cpp
3+
url = https://github.com/ggml-org/llama.cpp.git

app/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ android {
3030
}
3131

3232
ndk {
33-
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
33+
// Only 64-bit architectures - armv7 has issues with float16 NEON in llama.cpp
34+
abiFilters += listOf("arm64-v8a", "x86_64")
3435
}
3536
}
3637

app/consumer-rules.pro

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Consumer ProGuard rules for llama-kotlin-android library
2+
3+
# Keep native methods
4+
-keepclasseswithmembernames class * {
5+
native <methods>;
6+
}
7+
8+
# Keep the main API classes
9+
-keep class com.llamakotlin.android.LlamaModel { *; }
10+
-keep class com.llamakotlin.android.LlamaConfig { *; }
11+
-keep class com.llamakotlin.android.LlamaNative { *; }
12+
13+
# Keep exception classes
14+
-keep class com.llamakotlin.android.exception.** { *; }
15+
16+
# Keep callback interfaces
17+
-keep interface com.llamakotlin.android.** { *; }

app/src/main/cpp/CMakeLists.txt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ elseif(${ANDROID_ABI} STREQUAL "armeabi-v7a")
2020
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfpu=neon -mfloat-abi=softfp")
2121
endif()
2222

23-
# llama.cpp source directory (will be added as submodule)
23+
# ============================================================================
24+
# llama.cpp Submodule
25+
# ============================================================================
2426
set(LLAMA_CPP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/llama.cpp)
2527

26-
# Check if llama.cpp exists
28+
# Check if llama.cpp submodule exists
2729
if(EXISTS ${LLAMA_CPP_DIR}/CMakeLists.txt)
2830
message(STATUS "Found llama.cpp at ${LLAMA_CPP_DIR}")
2931

@@ -34,6 +36,8 @@ if(EXISTS ${LLAMA_CPP_DIR}/CMakeLists.txt)
3436
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
3537
set(LLAMA_STATIC ON CACHE BOOL "" FORCE)
3638
set(GGML_STATIC ON CACHE BOOL "" FORCE)
39+
set(LLAMA_NATIVE OFF CACHE BOOL "" FORCE) # Don't use native CPU features (cross-compile)
40+
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
3741

3842
# Add llama.cpp as subdirectory
3943
add_subdirectory(${LLAMA_CPP_DIR} llama.cpp)
@@ -44,11 +48,20 @@ if(EXISTS ${LLAMA_CPP_DIR}/CMakeLists.txt)
4448
${LLAMA_CPP_DIR}/ggml/include
4549
${LLAMA_CPP_DIR}/common
4650
)
51+
52+
set(LLAMA_AVAILABLE TRUE)
4753
else()
48-
message(WARNING "llama.cpp not found at ${LLAMA_CPP_DIR}. Using stub implementation.")
54+
message(WARNING "llama.cpp not found at ${LLAMA_CPP_DIR}")
55+
message(WARNING "Run: git submodule update --init --recursive")
56+
message(WARNING "Using stub implementation...")
57+
set(LLAMA_AVAILABLE FALSE)
4958
set(LLAMA_INCLUDE_DIRS "")
5059
endif()
5160

61+
# ============================================================================
62+
# JNI Library
63+
# ============================================================================
64+
5265
# JNI source files
5366
set(JNI_SOURCES
5467
llama_jni.cpp
@@ -64,25 +77,27 @@ target_include_directories(llama-android PRIVATE
6477
${LLAMA_INCLUDE_DIRS}
6578
)
6679

67-
# Find required libraries
80+
# Find required Android libraries
6881
find_library(log-lib log)
6982
find_library(android-lib android)
7083

7184
# Link libraries
72-
if(EXISTS ${LLAMA_CPP_DIR}/CMakeLists.txt)
85+
if(LLAMA_AVAILABLE)
7386
target_link_libraries(llama-android
7487
llama
7588
ggml
7689
${log-lib}
7790
${android-lib}
7891
)
7992
target_compile_definitions(llama-android PRIVATE LLAMA_AVAILABLE=1)
93+
message(STATUS "llama-android will link against llama.cpp")
8094
else()
8195
target_link_libraries(llama-android
8296
${log-lib}
8397
${android-lib}
8498
)
8599
target_compile_definitions(llama-android PRIVATE LLAMA_AVAILABLE=0)
100+
message(STATUS "llama-android will use stub implementation")
86101
endif()
87102

88103
# Export symbols

app/src/main/cpp/llama.cpp

Submodule llama.cpp added at 14931a8

app/src/main/cpp/llama_context_wrapper.cpp

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ bool LlamaContextWrapper::loadModel(const std::string& modelPath, const LlamaCon
5757
LOGI("Model params: gpu_layers=%d, use_mmap=%d, use_mlock=%d",
5858
config.gpuLayers, config.useMmap, config.useMlock);
5959

60-
// Load the model
61-
model_ = llama_load_model_from_file(modelPath.c_str(), modelParams);
60+
// Load the model using new API
61+
model_ = llama_model_load_from_file(modelPath.c_str(), modelParams);
6262
if (model_ == nullptr) {
6363
setError("Failed to load model from: " + modelPath);
6464
LOGE("%s", lastError_.c_str());
@@ -74,28 +74,22 @@ bool LlamaContextWrapper::loadModel(const std::string& modelPath, const LlamaCon
7474
ctxParams.n_threads = config.threads;
7575
ctxParams.n_threads_batch = config.threadsBatch;
7676

77-
if (config.seed >= 0) {
78-
ctxParams.seed = config.seed;
79-
} else {
80-
ctxParams.seed = static_cast<uint32_t>(std::time(nullptr));
81-
}
82-
83-
LOGI("Context params: n_ctx=%d, n_batch=%d, n_threads=%d, seed=%u",
84-
ctxParams.n_ctx, ctxParams.n_batch, ctxParams.n_threads, ctxParams.seed);
77+
LOGI("Context params: n_ctx=%d, n_batch=%d, n_threads=%d",
78+
ctxParams.n_ctx, ctxParams.n_batch, ctxParams.n_threads);
8579

86-
// Create context
87-
context_ = llama_new_context_with_model(model_, ctxParams);
80+
// Create context using new API
81+
context_ = llama_init_from_model(model_, ctxParams);
8882
if (context_ == nullptr) {
8983
setError("Failed to create llama context");
9084
LOGE("%s", lastError_.c_str());
91-
llama_free_model(model_);
85+
llama_model_free(model_);
9286
model_ = nullptr;
9387
return false;
9488
}
9589

9690
LOGI("Context created successfully");
9791

98-
// Set up sampler
92+
// Set up sampler with config seed
9993
setupSampler(config);
10094

10195
currentConfig_ = config;
@@ -130,7 +124,7 @@ void LlamaContextWrapper::unloadModel() {
130124
}
131125

132126
if (model_ != nullptr) {
133-
llama_free_model(model_);
127+
llama_model_free(model_);
134128
model_ = nullptr;
135129
LOGD("Model freed");
136130
}
@@ -200,19 +194,27 @@ void LlamaContextWrapper::generateStream(const std::string& prompt, TokenCallbac
200194
return;
201195
}
202196

203-
// Clear KV cache
204-
llama_kv_cache_clear(context_);
197+
// KV cache is already empty for a new generation
198+
// No need to clear explicitly
205199

206200
// Create batch for prompt processing
207201
llama_batch batch = llama_batch_init(cfg.batchSize, 0, 1);
208202

209-
// Add prompt tokens to batch
203+
// Add prompt tokens to batch manually (llama_batch_add was in common.h helper)
204+
batch.n_tokens = 0;
210205
for (size_t i = 0; i < promptTokens.size(); i++) {
211-
llama_batch_add(batch, promptTokens[i], i, { 0 }, false);
206+
batch.token[batch.n_tokens] = promptTokens[i];
207+
batch.pos[batch.n_tokens] = i;
208+
batch.n_seq_id[batch.n_tokens] = 1;
209+
batch.seq_id[batch.n_tokens][0] = 0;
210+
batch.logits[batch.n_tokens] = false;
211+
batch.n_tokens++;
212212
}
213213

214214
// Set logits for last token
215-
batch.logits[batch.n_tokens - 1] = true;
215+
if (batch.n_tokens > 0) {
216+
batch.logits[batch.n_tokens - 1] = true;
217+
}
216218

217219
// Process prompt
218220
if (llama_decode(context_, batch) != 0) {
@@ -227,13 +229,16 @@ void LlamaContextWrapper::generateStream(const std::string& prompt, TokenCallbac
227229
int n_cur = batch.n_tokens;
228230
int n_generated = 0;
229231

232+
// Get vocab for token operations
233+
const llama_vocab * vocab = llama_model_get_vocab(model_);
234+
230235
// Generation loop
231236
while (n_generated < cfg.maxTokens && !shouldCancel_) {
232237
// Sample next token
233238
llama_token newToken = llama_sampler_sample(sampler_, context_, -1);
234239

235240
// Check for end of generation
236-
if (llama_token_is_eog(model_, newToken)) {
241+
if (llama_vocab_is_eog(vocab, newToken)) {
237242
LOGI("End of generation token received");
238243
break;
239244
}
@@ -245,8 +250,13 @@ void LlamaContextWrapper::generateStream(const std::string& prompt, TokenCallbac
245250
callback(tokenStr);
246251

247252
// Prepare batch for next token
248-
llama_batch_clear(batch);
249-
llama_batch_add(batch, newToken, n_cur, { 0 }, true);
253+
batch.n_tokens = 0;
254+
batch.token[0] = newToken;
255+
batch.pos[0] = n_cur;
256+
batch.n_seq_id[0] = 1;
257+
batch.seq_id[0][0] = 0;
258+
batch.logits[0] = true;
259+
batch.n_tokens = 1;
250260

251261
// Decode
252262
if (llama_decode(context_, batch) != 0) {
@@ -275,7 +285,6 @@ void LlamaContextWrapper::generateStream(const std::string& prompt, TokenCallbac
275285
std::string word;
276286
while (iss >> word && !shouldCancel_) {
277287
callback(word + " ");
278-
// Small delay to simulate generation (would be removed in production)
279288
}
280289
#endif
281290

@@ -299,9 +308,7 @@ std::string LlamaContextWrapper::getLastError() const {
299308
std::string LlamaContextWrapper::getVersion() {
300309
#if LLAMA_AVAILABLE
301310
std::string version = LIBRARY_VERSION;
302-
version += " (llama.cpp build ";
303-
version += std::to_string(LLAMA_BUILD_NUMBER);
304-
version += ")";
311+
version += " (llama.cpp)";
305312
return version;
306313
#else
307314
return std::string(LIBRARY_VERSION) + " (stub)";
@@ -320,16 +327,16 @@ void LlamaContextWrapper::clearError() {
320327
#if LLAMA_AVAILABLE
321328

322329
std::vector<llama_token> LlamaContextWrapper::tokenize(const std::string& text, bool addBos) {
323-
// Get vocabulary size for initial allocation
324-
const int n_vocab = llama_n_vocab(model_);
330+
// Get vocab from model
331+
const llama_vocab * vocab = llama_model_get_vocab(model_);
325332

326333
// Estimate number of tokens (rough: 1 token per 4 chars)
327334
int n_tokens_estimate = text.length() / 4 + 16;
328335
std::vector<llama_token> tokens(n_tokens_estimate);
329336

330-
// Tokenize
337+
// Tokenize using vocab
331338
int n_tokens = llama_tokenize(
332-
model_,
339+
vocab,
333340
text.c_str(),
334341
text.length(),
335342
tokens.data(),
@@ -342,7 +349,7 @@ std::vector<llama_token> LlamaContextWrapper::tokenize(const std::string& text,
342349
// Need more space
343350
tokens.resize(-n_tokens);
344351
n_tokens = llama_tokenize(
345-
model_,
352+
vocab,
346353
text.c_str(),
347354
text.length(),
348355
tokens.data(),
@@ -364,10 +371,13 @@ std::vector<llama_token> LlamaContextWrapper::tokenize(const std::string& text,
364371
std::string LlamaContextWrapper::detokenize(const std::vector<llama_token>& tokens) {
365372
std::string result;
366373

374+
// Get vocab from model
375+
const llama_vocab * vocab = llama_model_get_vocab(model_);
376+
367377
for (llama_token token : tokens) {
368378
// Get token text
369379
char buf[256];
370-
int n = llama_token_to_piece(model_, token, buf, sizeof(buf) - 1, 0, true);
380+
int n = llama_token_to_piece(vocab, token, buf, sizeof(buf) - 1, 0, true);
371381

372382
if (n < 0) {
373383
LOGW("Failed to detokenize token: %d", token);
@@ -393,19 +403,14 @@ void LlamaContextWrapper::setupSampler(const LlamaConfig& config) {
393403

394404
// Add samplers in order
395405

396-
// Repetition penalty
406+
// Repetition penalty (new signature: 4 args)
397407
if (config.repeatPenalty != 1.0f) {
398408
llama_sampler_chain_add(sampler_,
399409
llama_sampler_init_penalties(
400-
llama_n_vocab(model_), // n_vocab
401-
llama_token_eos(model_), // special_eos_id
402-
llama_token_nl(model_), // linefeed_id
403410
64, // penalty_last_n
404411
config.repeatPenalty, // penalty_repeat
405412
0.0f, // penalty_freq
406-
0.0f, // penalty_present
407-
false, // penalize_nl
408-
false // ignore_eos
413+
0.0f // penalty_present
409414
)
410415
);
411416
}
@@ -425,7 +430,7 @@ void LlamaContextWrapper::setupSampler(const LlamaConfig& config) {
425430
llama_sampler_chain_add(sampler_, llama_sampler_init_temp(config.temperature));
426431
}
427432

428-
// Distribution sampling
433+
// Distribution sampling with seed
429434
uint32_t seed = config.seed >= 0 ? config.seed : static_cast<uint32_t>(std::time(nullptr));
430435
llama_sampler_chain_add(sampler_, llama_sampler_init_dist(seed));
431436

0 commit comments

Comments
 (0)