Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
3966c7e
feat: build llm/embed-llamacpp addons against qvac-fabric PR #144 (b9…
zoq Jun 1, 2026
ce1b964
build: update to the latest portfiles.
zoq Jun 1, 2026
bc79371
build: point to the correct fabric repo.
zoq Jun 1, 2026
e70bc6f
feat: wire MTP draft+verify+accept loop in llm-llamacpp addon.
zoq Jun 1, 2026
6a7de8c
fix: fence saveCacheToDisk write before model.unload() in tools-compa…
zoq Jun 1, 2026
41fb616
feat: accept positive reasoning-budget N as a token cap on the <think…
zoq Jun 2, 2026
69bde42
Merge branch 'main' into qvac-b9341
zoq Jun 2, 2026
9289481
build: extend qvac-fabric overlay-port to classification-ggml, vla-gg…
zoq Jun 2, 2026
5655f6a
fix: widen reasoning_budget TS type to number so positive token-cap v…
zoq Jun 3, 2026
8fd7f20
Point to the latest fabric changes.
zoq Jun 3, 2026
15ac661
fix: capture streamed output whenever tools are configured and feed p…
zoq Jun 3, 2026
ce2ad3b
Merge branch 'main' into qvac-b9341
zoq Jun 3, 2026
05a7a77
Point to the latest fabric changes.
zoq Jun 3, 2026
a76332e
Point to the latest fabric changes.
zoq Jun 3, 2026
8a6e7e1
feat: default flash attention to on across every non-bitnet, non-fine…
zoq Jun 4, 2026
3288b37
Point to the latest fabric changes.
zoq Jun 4, 2026
6559b4c
test: test: move runMultiInstanceTest into android.groupImagesPerf to…
zoq Jun 4, 2026
15c69cc
Merge branch 'main' into qvac-b9341
zoq Jun 5, 2026
13db8b5
Point to the latest fabric changes.
zoq Jun 5, 2026
520ded0
Revert workflow changes from MTP commit per review feedback.
zoq Jun 8, 2026
458b4ce
Merge branch 'main' into qvac-b9341
zoq Jun 8, 2026
54b3527
Merge branch 'main' into qvac-b9341
zoq Jun 8, 2026
0f3bd32
infra[notask]: bump qvac-fabric port refs
gianni-cor Jun 15, 2026
85d4175
Merge branch 'main' into qvac-b9341
gianni-cor Jun 15, 2026
91846a0
test[notask]: add continuous batching mobile runner
gianni-cor Jun 15, 2026
d4f2e6d
fix[notask]: capture streamed tool output
gianni-cor Jun 16, 2026
4d3c06e
Merge branch 'main' into qvac-b9341
gianni-cor Jun 17, 2026
b3d6861
infra[notask]: bump qvac-fabric port refs
gianni-cor Jun 18, 2026
7b7ac97
Merge branch 'main' into qvac-b9341
gianni-cor Jun 18, 2026
5c0497c
fix[notask]: remove incomplete MTP support
gianni-cor Jun 18, 2026
fe48d7d
fix[notask]: remove tool-call envelope wrapping
gianni-cor Jun 18, 2026
bcf2525
fix[notask]: remove speculative decoding leftovers
gianni-cor Jun 18, 2026
24740fd
Merge branch 'main' into qvac-b9341
gianni-cor Jun 18, 2026
706bb0b
fix[notask]: remove redundant tools compact capture
gianni-cor Jun 18, 2026
88a24d3
fix[notask]: use template reasoning budget tags
gianni-cor Jun 18, 2026
78a28f5
fix[notask]: restore existing comment punctuation
gianni-cor Jun 18, 2026
77f8084
Merge branch 'main' into qvac-b9341
gianni-cor Jun 18, 2026
890d92d
test[notask]: exercise per-request reasoning budget
gianni-cor Jun 19, 2026
ee8f4fd
test[notask]: cover reasoning budget helpers
gianni-cor Jun 19, 2026
3efda22
test[notask]: use safeTest for continuous batching
gianni-cor Jun 19, 2026
3287cd8
fix: satisfy llm CI lint checks
gianni-cor Jun 19, 2026
2247d0c
fix: match CI C++ formatting
gianni-cor Jun 19, 2026
1738430
test[notask]: match qwen thinking tag metadata
gianni-cor Jun 19, 2026
29b3dfe
test[notask]: run q8 mrope cache test on android
gianni-cor Jun 19, 2026
4968843
Merge branch 'main' into qvac-b9341
gianni-cor Jun 19, 2026
95f82f8
test[notask]: check reasoning budget token cap
gianni-cor Jun 19, 2026
8ae7d11
test[notask]: skip q8 mrope cache on android
gianni-cor Jun 19, 2026
f642fa5
Merge branch 'main' into qvac-b9341
gianni-cor Jun 19, 2026
cdfdf37
deps[notask]: bump qvac-fabric opencl fix
gianni-cor Jun 19, 2026
f1222fd
test[notask]: run q8 mrope cache test on android
gianni-cor Jun 19, 2026
cc94b2b
test[notask]: format reasoning budget test
gianni-cor Jun 19, 2026
c5885c6
deps[notask]: update qvac-fabric OpenCL fix
gianni-cor Jun 19, 2026
8481f8c
test[notask]: skip llm integration tests on darwin x64
gianni-cor Jun 19, 2026
f38b711
ci[notask]: use macos 15 xlarge for llm cpp tests
gianni-cor Jun 19, 2026
f9438b8
test[notask]: re-enable llm integration tests on windows
gianni-cor Jun 19, 2026
6bd5ae5
test[notask]: remove unused reasoning run options
gianni-cor Jun 19, 2026
4006ad4
deps[notask]: update qvac-fabric OpenCL copy fix
gianni-cor Jun 19, 2026
f5250c3
deps[notask]: align qvac-fabric ports
gianni-cor Jun 19, 2026
19b23eb
test[notask]: restore macos unit test sanitizer
gianni-cor Jun 19, 2026
da601db
Merge branch 'main' into qvac-b9341
gianni-cor Jun 19, 2026
f0cd76d
deps[notask]: update qvac-fabric OpenCL cache fix
gianni-cor Jun 20, 2026
09755af
test[notask]: skip q8 mrope cache on android
gianni-cor Jun 20, 2026
26ab6c3
deps[notask]: point qvac-fabric ports at temp branch
gianni-cor Jun 20, 2026
b19931a
test[notask]: simplify android q8 mrope skip
gianni-cor Jun 20, 2026
4746a9d
test[notask]: force sliding overflow on windows
gianni-cor Jun 20, 2026
eae8f71
Merge branch 'main' into qvac-b9341
gianni-cor Jun 22, 2026
bcce0d9
deps[notask]: remove qvac-fabric overlay ports
gianni-cor Jun 22, 2026
9f77d44
chore[notask]: restore embed cmake formatting
gianni-cor Jun 22, 2026
79e769e
fix[llm]: reject OpenCL quantized KV caches
gianni-cor Jun 22, 2026
49817ec
Merge branch 'main' into qvac-b9341
gianni-cor Jun 22, 2026
fb1be8d
fix[llm]: format OpenCL KV cache guard
gianni-cor Jun 22, 2026
6bc7bc4
fix[llm]: gate Metal KV cache guard on backend
gianni-cor Jun 22, 2026
bea0be1
fix[llm]: detect mtl Metal backend names
gianni-cor Jun 22, 2026
e53b5c9
Merge branch 'main' into qvac-b9341
gianni-cor Jun 22, 2026
9c71a59
Merge branch 'main' into qvac-b9341
gianni-cor Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cpp-tests-llm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
platform: linux
runner: qvac-ubuntu2404-x64
arch: x64
- os: macos-14
- os: macos-15-xlarge
platform: darwin
arch: arm64
- os: windows-2025
Expand Down
4 changes: 2 additions & 2 deletions packages/llm-llamacpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ if(BUILD_CLI)
target_include_directories(cli_tool PRIVATE ${QVAC_LIB_INFERENCE_ADDON_CPP_INCLUDE_DIRS})
# Set C++20 standard for std::views support
target_compile_features(cli_tool PRIVATE cxx_std_20)

# Install ggml backends next to the CLI tool binary in build folder
if((ANDROID OR UNIX) AND NOT APPLE)
foreach(_backend ${GGML_AVAILABLE_BACKENDS})
Expand All @@ -190,7 +190,7 @@ if(BUILD_TESTING)
find_package(GTest CONFIG REQUIRED)
include(GoogleTest)
enable_testing()

# Integration tests for model classes (includes backend selection tests)
# Pass ENABLE_COVERAGE option to test subdirectory
add_subdirectory(test/unit)
Expand Down
17 changes: 11 additions & 6 deletions packages/llm-llamacpp/addon/src/addon/AddonJs.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once
#include <any>
#include <cmath>
#include <functional>
#include <limits>
#include <memory>
#include <string>
#include <type_traits>
Expand Down Expand Up @@ -371,15 +373,18 @@ inline void parseGenerationParams(
auto reasoningBudget = configObj->getOptionalPropertyAs<js::Number, double>(
env, "reasoning_budget");
if (reasoningBudget.has_value()) {
// Exact compare (0 and -1 are representable doubles) so fractional
// inputs like 0.5 are rejected, not silently truncated.
if (*reasoningBudget != 0 && *reasoningBudget != -1) {
// Reject fractional inputs (0.5, -1.1, 32.7) by requiring the value to
// round-trip through int. -1 = unrestricted, 0 = disabled, N>0 caps the
// reasoning channel at N tokens via the budget sampler.
const double value = *reasoningBudget;
if (value < -1 || value != std::floor(value) ||
value > static_cast<double>(std::numeric_limits<int>::max())) {
throw StatusError(
general_error::InvalidArgument,
"generationParams.reasoning_budget must be -1 (unrestricted) "
"or 0 (disabled)");
"generationParams.reasoning_budget must be -1 (unrestricted), "
"0 (disabled), or a positive integer (token cap)");
}
overrides.reasoning_budget = static_cast<int>(*reasoningBudget);
overrides.reasoning_budget = static_cast<int>(value);
}
}

Expand Down
Comment thread
gianni-cor marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ void applyGenerationOverridesToSampling(
setIf(overrides.presence_penalty, sampling.penalty_present);
setIf(overrides.repeat_penalty, sampling.penalty_repeat);

// Forward reasoning_budget into the budget-sampler's token cap. The
// template-specific start/end/forced vectors are refreshed after prompt
// formatting, where common_chat_params exposes the thinking tags.
if (overrides.reasoning_budget) {
const int budget = *overrides.reasoning_budget;
if (budget > 0) {
sampling.reasoning_budget_tokens = budget;
} else {
sampling.reasoning_budget_tokens = -1;
sampling.reasoning_budget_start.clear();
sampling.reasoning_budget_end.clear();
sampling.reasoning_budget_forced.clear();
}
}

// `json_schema` and `grammar` are mutually exclusive at the JS boundary
// and in `AddonJs::runJob::parseText`, so reaching this branch with both
// set means a caller bypassed those checks (most likely the C++ unit
Expand Down
82 changes: 45 additions & 37 deletions packages/llm-llamacpp/addon/src/model-interface/LlamaModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ void LlamaModel::resolveShardPaths(
void LlamaModel::tuneConfigMap(
std::unordered_map<std::string, std::string>& configFilemap,
const ModelMetaData& metadata, const std::optional<int>& adrenoVersion,
const FinetuneConfigOverrides& finetuneOverrides, bool isOpenCl) {
const FinetuneConfigOverrides& finetuneOverrides, bool isOpenCl,
bool isMetal) {

const bool isFinetuning = finetuneOverrides.active;

Expand Down Expand Up @@ -143,13 +144,11 @@ void LlamaModel::tuneConfigMap(
QLOG_IF(
Priority::INFO,
"[LlamaModel] BitNet model detected: disabling flash attention\n");
} else if (isOpenCl && notUserSet("flash-attn", "flash_attn")) {
} else if (notUserSet("flash-attn", "flash_attn")) {
configFilemap.erase("flash_attn");
configFilemap["flash-attn"] = "off";
configFilemap["flash-attn"] = "on";
QLOG_IF(
Priority::INFO,
"[LlamaModel] OpenCL backend selected: disabling flash attention by "
"default (not reliably supported on OpenCL)\n");
Priority::INFO, "[LlamaModel] Enabling flash attention by default\n");
}

constexpr int kAdrenoUbatchThreshold = 800;
Expand Down Expand Up @@ -213,22 +212,18 @@ void LlamaModel::tuneConfigMap(
}
}

// TurboQuant / PolarQuant KV-cache types (tbq3_0 / tbq4_0 / pq3_0 / pq4_0,
// ship Vulkan + CPU implementations only. On the OpenCL backend
// the kernels don't exist; on Metal the standalone MUL_MAT pipelines are
// explicitly disabled. Silently letting the user proceed lets llama.cpp
// commit KV-cache tensors to a backend that can't run the ops on them,
// which then aborts in ggml_backend_sched_split_graph at model load.
// Surface a clean error here instead.
#if defined(__APPLE__)
constexpr bool kIsMetal = true;
#else
constexpr bool kIsMetal = false;
#endif
if (isOpenCl || kIsMetal) {
// Quantized KV-cache types are fragile on OpenCL: standard q-cache types can
// fail later during cache shifts, while TBQ/PQ kernels are not implemented.
// Surface a clean error here instead of letting llama.cpp commit KV-cache
// tensors to a backend that can't run the required ops.
if (isOpenCl || isMetal) {
auto isTurboQuantKvType = [](const std::string& v) {
return v == "tbq3_0" || v == "tbq4_0" || v == "pq3_0" || v == "pq4_0";
};
auto isQuantizedKvType = [&](const std::string& v) {
return isTurboQuantKvType(v) || v == "q4_0" || v == "q4_1" ||
v == "q5_0" || v == "q5_1" || v == "q8_0" || v == "iq4_nl";
};
auto checkCacheType = [&](const char* hyphenKey,
const char* underscoreKey,
const char* side) {
Expand All @@ -237,20 +232,30 @@ void LlamaModel::tuneConfigMap(
it = configFilemap.find(underscoreKey);
if (it == configFilemap.end())
return;
if (!isTurboQuantKvType(it->second))
if (isOpenCl) {
if (!isQuantizedKvType(it->second))
return;
} else if (!isTurboQuantKvType(it->second)) {
return;
}
const char* backendName = isOpenCl ? "OpenCL" : "Metal";
const char* typeName = isTurboQuantKvType(it->second)
? "TurboQuant/PolarQuant"
: "quantized";
const char* alternatives =
isOpenCl ? "f32/f16/bf16"
: "f32/f16/bf16/q4_0/q4_1/q5_0/q5_1/q8_0/iq4_nl";
throw qvac_errors::StatusError(
qvac_errors::general_error::InvalidArgument,
string_format(
"[LlamaModel] cache-type-%s=%s is a TurboQuant/PolarQuant type "
"and is not supported on the %s backend (Vulkan and CPU only). "
"Either pick a different cache type (f32/f16/bf16/q4_0/q4_1/"
"q5_0/q5_1/q8_0/iq4_nl) or switch device to a Vulkan GPU or "
"CPU.\n",
"[LlamaModel] cache-type-%s=%s is a %s KV-cache type and is not "
"supported on the %s backend. Either pick a different cache "
"type (%s) or switch device to a Vulkan GPU or CPU.\n",
side,
it->second.c_str(),
backendName));
typeName,
backendName,
alternatives));
};
checkCacheType("cache-type-k", "cache_type_k", "k");
checkCacheType("cache-type-v", "cache_type_v", "v");
Expand Down Expand Up @@ -994,21 +999,23 @@ void LlamaModel::commonParamsParse(
"embedded chat template is applied\n");
}

// reasoning-budget controls whether the model emits a <think> reasoning
// channel. -1 (default) leaves it on; 0 disables. `std::from_chars` is used
// instead of `std::stoi` because the latter accepts trailing garbage ("0abc"
// → 0) and throws an uncaught `std::out_of_range` on overflow.
// reasoning-budget controls the size of the model's <think> reasoning
// channel: -1 (default) leaves it unrestricted, 0 disables thinking
// entirely, any positive N caps the reasoning channel at N tokens (the
// budget sampler forces </think> once N reasoning tokens have been
// emitted).
auto parseReasoningBudget = [](const std::string& raw) {
int value = 0;
const char* begin = raw.data();
const char* end = begin + raw.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec != std::errc{} || ptr != end || (value != 0 && value != -1)) {
if (ec != std::errc{} || ptr != end || value < -1) {
throw qvac_errors::StatusError(
ADDON_ID,
qvac_errors::general_error::toString(
qvac_errors::general_error::InvalidArgument),
"reasoning-budget must be -1 (unrestricted) or 0 (disabled)");
"reasoning-budget must be -1 (unrestricted), 0 (disabled), or a "
"positive integer (token cap)");
}
return value;
};
Expand Down Expand Up @@ -1114,6 +1121,7 @@ void LlamaModel::commonParamsParse(
}

bool isOpenCl = false;
bool isMetal = false;
{
using namespace backend_selection;
const BackendType preferredBackend =
Expand Down Expand Up @@ -1180,14 +1188,18 @@ void LlamaModel::commonParamsParse(

isOpenCl = chosenBackend.first == BackendType::GPU &&
chosenBackend.second.find("opencl") != std::string::npos;
isMetal = chosenBackend.first == BackendType::GPU &&
(chosenBackend.second.find("metal") != std::string::npos ||
chosenBackend.second.rfind("mtl", 0) == 0);
}

tuneConfigMap(
configFilemap,
metadata_,
outAdrenoVersion,
pendingFinetuneOverrides_,
isOpenCl);
isOpenCl,
isMetal);

// Handle both reverse-prompt variants
for (const std::string& key : {"reverse-prompt", "reverse_prompt"}) {
Expand Down Expand Up @@ -1310,10 +1322,6 @@ void LlamaModel::commonParamsParse(
postprocess_cpu_params(params.cpuparams, nullptr);
postprocess_cpu_params(params.cpuparams_batch, &params.cpuparams);

postprocess_cpu_params(params.speculative.draft.cpuparams, &params.cpuparams);
postprocess_cpu_params(
params.speculative.draft.cpuparams_batch, &params.cpuparams_batch);

if (!params.kv_overrides.empty()) {
params.kv_overrides.emplace_back();
params.kv_overrides.back().key[0] = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,15 @@ class LlamaModel : public IModel, public IModelAsyncLoad, public IModelCancel {
/// @param adrenoVersion Detected Adreno GPU version, if any.
/// @param finetuneOverrides If set, finetuning mode is active with these
/// context/batch params and GPU caps.
/// @param isOpenCl True when the chosen GPU backend is OpenCL; used to
/// disable flash-attn by default since it is not reliably supported on
/// the OpenCL backend.
/// @param isOpenCl True when the chosen GPU backend is OpenCL; used to reject
/// unsupported quantized KV-cache types.
/// @param isMetal True when the chosen GPU backend is Metal; used to reject
/// unsupported TurboQuant/PolarQuant KV-cache types.
static void tuneConfigMap(
std::unordered_map<std::string, std::string>& configFilemap,
const ModelMetaData& metadata, const std::optional<int>& adrenoVersion,
const FinetuneConfigOverrides& finetuneOverrides = {},
bool isOpenCl = false);
bool isOpenCl = false, bool isMetal = false);

/**
* The Constructor for llama model.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ struct GenerationParams {
// Mutually exclusive with `grammar` — the JS wrapper rejects requests
// that set both. Mirrors the load-time `--json-schema` flag.
std::optional<std::string> json_schema;
// Reasoning channel budget override. `-1` keeps reasoning on, `0` disables
// it for this request. Mirrors the load-time `reasoning-budget` config; the
// override is applied to `params_.reasoning_budget` for the duration of the
// request and restored on completion.
// Reasoning channel budget override. `-1` keeps reasoning unrestricted, `0`
// disables it, and positive values cap the reasoning channel at that many
// tokens. Mirrors the load-time `reasoning-budget` config; the override is
// applied to `params_.reasoning_budget` for the duration of the request and
// restored on completion.
std::optional<int> reasoning_budget;

[[nodiscard]] bool hasOverrides() const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,42 @@ void MtmdLlmContext::tokenizeChat(
if (!tools.empty()) {
inputs.tools = tools;
}
formattedChat = getPrompt(tmpls_.get(), inputs, &thinkingForcedOpen_);
std::string thinkingStartTag;
std::string thinkingEndTag;
std::string generationPrompt;
formattedChat = getPrompt(
tmpls_.get(),
inputs,
&thinkingForcedOpen_,
&thinkingStartTag,
&thinkingEndTag,
&generationPrompt);
thinkingForcedOpenText_ =
thinkingForcedOpen_
? getThinkingForcedOpenText(generationPrompt, thinkingStartTag)
: std::string{};

if (formattedChat.empty()) {
std::string errorMsg = string_format(
"[MtmdLlm] %s: formatted chat prompt is empty\n", __func__);
throw qvac_errors::StatusError(ADDON_ID, toString(EmptyPrompt), errorMsg);
}

if (configureReasoningBudgetSampling(
params_,
modelCtx_.lctx,
thinkingStartTag,
thinkingEndTag,
generationPrompt)) {
smpl_.reset(common_sampler_init(modelCtx_.model, params_.sampling));
if (!smpl_) {
std::string errorMsg = string_format(
"[MtmdLlm] %s: failed to initialize sampling subsystem\n", __func__);
throw qvac_errors::StatusError(
ADDON_ID, toString(UnableToCreateSamplingSystem), errorMsg);
}
}

QLOG_IF(
Priority::DEBUG,
string_format("[MtmdLlm] formatted prompt: %s\n", formattedChat.c_str()));
Expand Down Expand Up @@ -492,10 +520,10 @@ bool MtmdLlmContext::generateResponse(
if (thinkingForcedOpen_ && outputCallback) {
// MtmdLlmContext doesn't carry a reasoningState_ (no reasoning-aware EOS
// replacement on the multimodal path today), so unlike TextLlmContext we
// only prepend the visible "<think>\n" opener and don't flip an
// only prepend the visible reasoning opener and don't flip an
// inside_reasoning flag. If reasoning state is added here later, mirror
// TextLlmContext::generateResponse and set it true alongside this emit.
outputCallback("<think>\n");
outputCallback(thinkingForcedOpenText_);
}

if (stopGeneration_.load()) {
Expand Down Expand Up @@ -717,6 +745,8 @@ void MtmdLlmContext::resetState(bool resetStats) {

// Clear UTF-8 buffer when resetting state
utf8Buffer_.clear();
thinkingForcedOpen_ = false;
thinkingForcedOpenText_.clear();

clearSequenceMemory(modelCtx_.lctx);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,11 @@ class MtmdLlmContext : public LlmContext {
bool isHarmonyModel_ = false;
llama_token harmonyCallToken_ = LLAMA_TOKEN_NULL;

// Force-opens the reasoning channel in the prompt suffix to prepend the
// matching "<think>\n" opener to the visible stream so consumers see balanced
// Force-opens the reasoning channel in the prompt suffix. The text mirrors
// the template-specific visible reasoning opener so consumers see balanced
// tags.
bool thinkingForcedOpen_ = false;
std::string thinkingForcedOpenText_;

std::atomic<bool> stopGeneration_ = false;
};
Loading
Loading