Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5
ADFA-5188 | Enable KV cache quantization and flash attention#76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
c45cce124b31acf570d095d1f91cFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -365,9 +365,30 @@ Java_android_llama_cpp_LLamaAndroid_free_1model(JNIEnv *, jobject, jlong model) | ||
| llama_model_free(reinterpret_cast<llama_model *>(model)); | ||
| } | ||
| /** | ||
| * Backstops a context size Kotlin chose: a misparsed header must not ask for more than the model | ||
| * was trained for, and a non-positive argument falls back to the default. Never clamps below | ||
| * DEFAULT_N_CTX, the context a 2048-trained model always got, so no prompt that fit regresses. | ||
| * | ||
| * @param requested the context asked for, in tokens | ||
| * @param trained_ctx what the model was trained for, or 0 when it does not say | ||
| * @return the context to configure, never above trained_ctx unless that is below DEFAULT_N_CTX | ||
| */ | ||
| static int clamp_context(int requested, int trained_ctx) { | ||
| int clamped = requested > 0 ? requested : DEFAULT_N_CTX; | ||
| const int ceiling = std::max(trained_ctx, DEFAULT_N_CTX); | ||
| if (trained_ctx > 0 && clamped > ceiling) { | ||
| LOGi("context: n_ctx %d exceeds the model's trained %d; clamping to %d", clamped, | ||
| trained_ctx, ceiling); | ||
| clamped = ceiling; | ||
| } | ||
| return clamped; | ||
| } | ||
| extern "C" | ||
| JNIEXPORT jlong JNICALL | ||
| Java_android_llama_cpp_LLamaAndroid_new_1context(JNIEnv *env, jobject, jlong jmodel, jint jn_ctx) { | ||
| Java_android_llama_cpp_LLamaAndroid_new_1context(JNIEnv *env, jobject, jlong jmodel, jint jn_ctx, | ||
| jboolean jquantize_kv, jint jfallback_n_ctx) { | ||
| auto model = reinterpret_cast<llama_model *>(jmodel); | ||
| if (!model) { | ||
| @@ -389,23 +410,68 @@ Java_android_llama_cpp_LLamaAndroid_new_1context(JNIEnv *env, jobject, jlong jmo | ||
| llama_context_params ctx_params = llama_context_default_params(); | ||
| int requested_ctx = jn_ctx > 0 ? jn_ctx : DEFAULT_N_CTX; | ||
| // Backstop on Kotlin's number: a misparsed header must not exceed the trained context. Floored | ||
| // at DEFAULT_N_CTX, the context a 2048-trained model always got, so no prompt that fit regresses. | ||
| const int trained_ctx = llama_model_n_ctx_train(model); | ||
| const int clamp_ctx = std::max(trained_ctx, DEFAULT_N_CTX); | ||
| if (trained_ctx > 0 && requested_ctx > clamp_ctx) { | ||
| LOGi("context: requested n_ctx %d exceeds the model's trained %d; clamping to %d", | ||
| requested_ctx, trained_ctx, clamp_ctx); | ||
| requested_ctx = clamp_ctx; | ||
| const int requested_ctx = clamp_context(jn_ctx, trained_ctx); | ||
| // Sized by Kotlin against f16, the type the fallback below drops to; the two sizes differ | ||
| // because f16 costs nearly twice as much per cached token. | ||
| const int fallback_ctx = clamp_context(jfallback_n_ctx, trained_ctx); | ||
| const bool quantize_kv = jquantize_kv == JNI_TRUE; | ||
| // AUTO rather than ENABLED: it is AUTO that makes llama.cpp validate a quantized cache against | ||
| // the model's head width and refuse it by returning null. ENABLED skips that check and aborts | ||
| // inside ggml instead, taking the IDE down with it. | ||
| ctx_params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_AUTO; | ||
| if (quantize_kv) { | ||
| // A quantized V cache is only defined with flash attention, which AUTO may still refuse. | ||
| ctx_params.type_k = GGML_TYPE_Q8_0; | ||
| ctx_params.type_v = GGML_TYPE_Q8_0; | ||
| } | ||
| ctx_params.n_ctx = requested_ctx; | ||
| ctx_params.n_threads = n_threads; | ||
| ctx_params.n_threads_batch = n_threads_batch; | ||
| LOGi("Creating context: n_ctx = %d (model trained for %d), kv cache = %s", requested_ctx, | ||
| trained_ctx, quantize_kv ? "q8_0" : "f16"); | ||
| llama_context *context = llama_init_from_model(model, ctx_params); | ||
| bool quantized_in_use = quantize_kv; | ||
| // Two unrelated failures land here and want opposite retries: a refused quantized cache is not | ||
| // a shortage and keeps its long context, while a shortage is answered only by fewer bytes. | ||
| if (!context && quantize_kv) { | ||
| // f16 with flash attention off is the one configuration nothing here can refuse — no | ||
| // block-size constraint on the cache, and no graph for AUTO to fail to place. It costs the | ||
| // attention speed-up on a model whose only problem was the cache type, which is the cheaper | ||
| // mistake to make. Kotlin already screens the head width, so getting here at all means the | ||
| // header and llama.cpp disagreed. | ||
| LOGe("Context creation failed; retrying at f16 with flash attention off and n_ctx %d", | ||
| fallback_ctx); | ||
| ctx_params.type_k = GGML_TYPE_F16; | ||
| ctx_params.type_v = GGML_TYPE_F16; | ||
| ctx_params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED; | ||
| ctx_params.n_ctx = fallback_ctx; | ||
jatezzz marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| context = llama_init_from_model(model, ctx_params); | ||
| quantized_in_use = false; | ||
| } | ||
| // The only retry that shrinks the allocation, back to the context every load got before this was | ||
| // sized per device; fallback_ctx cannot, since f16 costs what q8_0 bought the extra tokens with. | ||
| // The guard skips an attempt that would re-request exactly what just failed. | ||
| // n_ctx is unsigned; every value compared here is a clamped positive. | ||
| const int current_ctx = (int) ctx_params.n_ctx; | ||
| const int floor_ctx = std::min(current_ctx, DEFAULT_N_CTX); | ||
| if (!context && (floor_ctx < current_ctx || | ||
| ctx_params.flash_attn_type != LLAMA_FLASH_ATTN_TYPE_DISABLED)) { | ||
| LOGe("Context creation failed; retrying at the n_ctx %d floor with f16 and flash attention off", | ||
| floor_ctx); | ||
| ctx_params.type_k = GGML_TYPE_F16; | ||
| ctx_params.type_v = GGML_TYPE_F16; | ||
| ctx_params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED; | ||
| ctx_params.n_ctx = floor_ctx; | ||
| context = llama_init_from_model(model, ctx_params); | ||
| quantized_in_use = false; | ||
| } | ||
| if (!context) { | ||
| LOGe("context: llama_new_context_with_model() returned null"); | ||
| @@ -414,9 +480,11 @@ Java_android_llama_cpp_LLamaAndroid_new_1context(JNIEnv *env, jobject, jlong jmo | ||
| return 0; | ||
| } | ||
| // n_ctx now varies per model and device, so a wrong size is invisible in a report without this. | ||
| LOGi("context: created with n_ctx = %u (requested %d, model trained for %d), n_batch = %u", | ||
| llama_n_ctx(context), requested_ctx, trained_ctx, llama_n_batch(context)); | ||
| // n_ctx and the cache type now vary per model and device, so a wrong one is invisible in a | ||
| // report without this. | ||
| LOGi("Context created: n_ctx = %u (requested %d, model trained for %d), n_batch = %u, kv cache = %s", | ||
| llama_n_ctx(context), (int) jn_ctx, trained_ctx, llama_n_batch(context), | ||
| quantized_in_use ? "q8_0" : "f16"); | ||
| // A fresh context has an empty KV cache, so the prefix record must start empty too. | ||
| { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| package com.itsaky.androidide.plugins.aiagentlocal.model | ||
| /** | ||
| * Picks the context size (`n_ctx`) one model load gets, from what the model advertises and what the | ||
| * device can spare — the KV cache scales linearly with it and is the largest knob we control. Pure | ||
| * and Android-free, so every boundary is unit-testable off-device. See ADFA-5187. | ||
| * Picks what one model load gets: the context size (`n_ctx`) and the type the KV cache is stored as, | ||
| * from what the model advertises and what the device can spare. Pure and Android-free, so every | ||
| * boundary is unit-testable off-device. See ADFA-5187 and ADFA-5188. | ||
| */ | ||
| object ContextSizePolicy { | ||
| @@ -34,15 +34,34 @@ object ContextSizePolicy { | ||
| */ | ||
| private const val KV_BUDGET_DIVISOR = 2L | ||
| /** | ||
| * The cache type a load should ask for. Quantized wherever the model allows it: it halves the | ||
| * bytes one cached token costs, which is what lets [choose] return a longer context on the same | ||
| * device. Falls back to [KvCacheType.F16] rather than risking a refused context. See ADFA-5188. | ||
| * | ||
| * @param header the model's GGUF metadata, or null when it could not be read | ||
| * @return the type to configure natively, and to size the context against | ||
| */ | ||
| fun chooseKvCache(header: GgufHeader?): KvCacheType = | ||
| if (KvCacheType.Q8_0.supports(header)) KvCacheType.Q8_0 else KvCacheType.F16 | ||
| /** | ||
| * @param header the model's GGUF metadata, or null when it could not be read | ||
| * @param availableBytes free RAM right now, or null when it could not be read; a negative | ||
| * reading is treated as unreadable too | ||
| * @param modelSizeBytes the model file's size, or null when it could not be read | ||
| * @param modelSizeBytes the model file's size, or null when it could not be read; the weights | ||
| * are charged against free RAM before the cache gets a budget | ||
| * @param kvType the cache type this load will ask for, from [chooseKvCache]; the budget buys | ||
| * about twice the context under [KvCacheType.Q8_0], so the two have to be decided together | ||
| * @return the context to configure, always between [DEFAULT_CONTEXT_TOKENS] and | ||
| * [MAX_CONTEXT_TOKENS] inclusive | ||
| */ | ||
| fun choose(header: GgufHeader?, availableBytes: Long?, modelSizeBytes: Long?): Int { | ||
| fun choose( | ||
| header: GgufHeader?, | ||
| availableBytes: Long?, | ||
| modelSizeBytes: Long?, | ||
| kvType: KvCacheType = KvCacheType.F16, | ||
| ): Int { | ||
| // Each null is a distinct "we don't know"; all of them mean the same fallback. | ||
| if (header == null) return DEFAULT_CONTEXT_TOKENS | ||
| // A negative reading is not free RAM this can reason about, so treat it as unreadable. | ||
| @@ -52,7 +71,7 @@ object ContextSizePolicy { | ||
| // Nothing to weigh below the floor, and no reason to price a cache we would not shrink. | ||
| if (modelTokens <= DEFAULT_CONTEXT_TOKENS) return DEFAULT_CONTEXT_TOKENS | ||
| val perToken = ModelMemoryEstimator.kvBytesPerToken(header)?.takeIf { it > 0L } | ||
| val perToken = ModelMemoryEstimator.kvBytesPerToken(header, kvType)?.takeIf { it > 0L } | ||
jatezzz marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ?: return DEFAULT_CONTEXT_TOKENS | ||
| // Weights first, then the compute buffers, each clamped at zero rather than left to run | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.