Compare commits

..
Author SHA1 Message Date
8c2c4c39c5 ggml-vulkan: add optional device keepalive (#29267)
* ggml-vulkan: add optional device keepalive

* simplify implementation, move ownership to backend, change env var to seconds

---------

Co-authored-by: Acmmi <acmmi@Acmmis-MacBook-Air.local>
Co-authored-by: Ruben Ortlam <rortlam@redhat.com>
2026-11-13 13:53:55 +02:00
o7siandPiotr Wilkin 108f73adfe grammar : optimize matching with left factoring (#29557)
* grammar : limit the number of alternatives

* grammar : introduce an AST for grammar parsing

* grammar : revert AST refactor and alternatives limit

This reverts commits:
- 84f3b57c4d
- 393a707ebb

* grammar: optimize big grammar matching by using left-factorization

* grammar : preserve character class boundaries during left factoring

* grammar : prevent infinite expansion during left factoring

* grammar : use ASCII characters in comments

* grammar : test empty suffixes after left factoring

* grammar : deduplicate left factoring logic

* grammar : add comments

* grammar : use number parameters and clarify RSS output

* grammar : clarify character class test names

* grammar : remove performance benchmark

* grammar : share immutable rules

---------

Co-authored-by: Piotr Wilkin <piotr.wilkin@syndatis.com>
2026-11-13 13:52:21 +02:00
15 changed files with 259 additions and 73 deletions
+4
View File
@@ -1279,6 +1279,10 @@ struct ggml_backend_vk_context {
vk_device device;
// Workaround for drivers moving VRAM contents off the GPU when idle
std::promise<void> keepalive_stop;
std::thread keepalive_thread;
size_t semaphore_idx, binary_semaphore_idx, event_idx;
ggml_vk_garbage_collector gc;
size_t prealloc_size_x, prealloc_size_y, prealloc_size_split_k, prealloc_size_add_rms_partials, prealloc_size_add_rms_partials_offset;
+29
View File
@@ -4237,6 +4237,8 @@ static bool ggml_vk_has_rebar(const vk::PhysicalDevice & physical_device) {
return false;
}
static void ggml_vk_device_start_keepalive(vk_device& device);
vk_device ggml_vk_get_device(size_t idx) {
VK_LOG_DEBUG("ggml_vk_get_device(" << idx << ")");
@@ -13353,6 +13355,10 @@ void ggml_vk_graph_cleanup(ggml_backend_vk_context * ctx) {
void ggml_vk_cleanup(ggml_backend_vk_context * ctx) {
VK_LOG_DEBUG("ggml_vk_cleanup(" << ctx->name << ")");
if (ctx->keepalive_thread.joinable()) {
ctx->keepalive_stop.set_value();
ctx->keepalive_thread.join();
}
// discard any unsubmitted command buffers
ctx->compute_ctx.reset();
// wait for any pending command buffers to finish
@@ -15719,11 +15725,34 @@ static ggml_guid_t ggml_backend_vk_guid() {
return &guid;
}
static void ggml_vk_start_keepalive(ggml_backend_vk_context * ctx) {
const char * env = getenv("GGML_VK_KEEPALIVE");
const int s = env ? atoi(env) : 0;
if (s <= 0) {
return;
}
GGML_LOG_INFO("ggml_vulkan: GGML_VK_KEEPALIVE=%ds on %s\n", s, ctx->device->name.c_str());
vk_buffer buf = ggml_vk_create_buffer_check(ctx->device, 4, vk::MemoryPropertyFlagBits::eDeviceLocal);
ctx->keepalive_thread = std::thread([buf, stop = ctx->keepalive_stop.get_future(), s]() mutable {
while (stop.wait_for(std::chrono::seconds(s)) == std::future_status::timeout) {
try {
ggml_vk_buffer_memset(buf, 0, 0, 4);
} catch (const std::exception & e) {
GGML_LOG_WARN("ggml_vulkan: keepalive stopped: %s\n", e.what());
return;
}
}
});
}
ggml_backend_t ggml_backend_vk_init(size_t dev_num) {
VK_LOG_DEBUG("ggml_backend_vk_init(" << dev_num << ")");
ggml_backend_vk_context * ctx = new ggml_backend_vk_context;
ggml_vk_init(ctx, dev_num);
ggml_vk_start_keepalive(ctx);
ggml_backend_t vk_backend = new ggml_backend {
/* .guid = */ ggml_backend_vk_guid(),
+4 -7
View File
@@ -6,7 +6,7 @@ version = "0.0.0"
dynamic = ["classifiers"]
readme = "README.md"
authors = [{name = "GGML", email = "ggml@ggml.ai"}]
requires-python = '>=3.10,<3.16'
requires-python = '>=3.10,<3.15'
dependencies = [
'numpy (>=2.2.6,<3.0.0); python_version < "3.14"',
'numpy (>=2.3.0,<3.0.0); python_version >= "3.14"',
@@ -40,12 +40,9 @@ packages = [
[tool.poetry.dependencies]
torch = [
{ version = "==2.11.0", source = "pypi", markers = "sys_platform == 'darwin' and python_version < '3.15'" },
{ version = "==2.11.0+cpu", source = "pytorch", markers = "sys_platform == 'linux' and python_version < '3.15'" },
{ version = "==2.11.0", source = "pypi", markers = "sys_platform == 'win32' and python_version < '3.15'" },
{ version = "==2.15.0", source = "pypi", markers = "sys_platform == 'darwin' and python_version >= '3.15'" },
{ version = "==2.15.0+cpu", source = "pytorch", markers = "sys_platform == 'linux' and python_version >= '3.15'" },
{ version = "==2.15.0", source = "pypi", markers = "sys_platform == 'win32' and python_version >= '3.15'" },
{ version = "==2.11.0", source = "pypi", markers = "sys_platform == 'darwin'" },
{ version = "==2.11.0+cpu", source = "pytorch", markers = "sys_platform == 'linux'" },
{ version = "==2.11.0", source = "pypi", markers = "sys_platform == 'win32'" },
]
[tool.poetry.group.dev.dependencies]
@@ -1,4 +1,3 @@
tabulate~=0.9.0
GitPython~=3.1.43
matplotlib~=3.10.0; python_version < "3.11"
matplotlib~=3.11.2; python_version >= "3.11"
matplotlib~=3.10.0
@@ -2,5 +2,4 @@
--extra-index-url https://download.pytorch.org/whl/cpu
## Embedding Gemma requires PyTorch 2.6.0 or later, bumped to 2.11.0 for compatibility
torch==2.11.0; python_version < "3.15"
torch==2.15.0; python_version >= "3.15"
torch==2.11.0
@@ -1,5 +1,5 @@
numpy~=2.2.6; python_version < "3.14"
numpy~=2.5.4; python_version >= "3.14"
numpy~=2.4.6; python_version >= "3.14"
sentencepiece>=0.1.98,<0.3.0
transformers==4.57.6
@@ -1,4 +1,4 @@
numpy~=2.2.6; python_version < "3.14"
numpy~=2.5.4; python_version >= "3.14"
numpy~=2.4.6; python_version >= "3.14"
PySide6~=6.9.0
gguf>=0.17.0
+2 -3
View File
@@ -1,7 +1,6 @@
datasets~=4.8.0
matplotlib~=3.10.0; python_version < "3.11"
matplotlib~=3.11.2; python_version >= "3.11"
matplotlib~=3.10.0
numpy~=2.2.6; python_version < "3.14"
numpy~=2.5.4; python_version >= "3.14"
numpy~=2.4.6; python_version >= "3.14"
requests~=2.32.3
tqdm~=4.67.1
@@ -1 +1 @@
cffi~=2.1.1
cffi~=1.16.0
+3 -5
View File
@@ -1,11 +1,9 @@
pytest~=8.3.3
matplotlib~=3.10.0; python_version < "3.11"
matplotlib~=3.11.2; python_version >= "3.11"
matplotlib~=3.10.0
numpy~=2.2.6; python_version < "3.14"
numpy~=2.5.4; python_version >= "3.14"
numpy~=2.4.6; python_version >= "3.14"
openai~=2.14.0
pandas~=2.3.3; python_version < "3.11"
pandas~=3.0.6; python_version >= "3.11"
pandas~=2.3.3
prometheus-client~=0.20.0
requests~=2.32.3
wget~=3.2
+179 -43
View File
@@ -948,6 +948,160 @@ static void llama_grammar_advance_stack(
}
}
// Left-factor grammar rules to reduce redundant parallel stacks.
//
// When a rule has many alternates (e.g. tool-choice ::= tc0 | tc1 | ... | tc119)
// and the referenced rules share a common element-content prefix, extract that
// prefix so the parser processes it once instead of N times.
//
// Handles two cases:
// 1. Inline common prefix: rule = "ab1" | "ab2" -> rule = "ab" rule', rule' = "1" | "2"
// 2. Cross-rule prefix: rule = A | B | C where A,B,C start with same elements
// -> rule = common_prefix rule', rule' = A_suffix | B_suffix | C_suffix
static void llama_grammar_left_factor(llama_grammar_rules & rules) {
auto elem_eq = [](const llama_grammar_element & a, const llama_grammar_element & b) {
return a.type == b.type && a.value == b.value;
};
// Keep the prefix boundary outside character classes.
auto trim_prefix = [](const llama_grammar_rule & rule, size_t start, size_t length) {
while (length > 0) {
const auto type = rule[start + length].type;
if (type != LLAMA_GRETYPE_CHAR_ALT && type != LLAMA_GRETYPE_CHAR_RNG_UPPER) {
break;
}
length--;
}
return length;
};
struct alt_info {
size_t rule_idx;
size_t start;
size_t end;
};
const size_t n_rules_original = rules.size();
// Original rules factor at most once; generated rules do not resolve references.
// Each new suffix removes a non-empty prefix.
bool changed = true;
while (changed) {
changed = false;
const size_t n_rules = rules.size();
for (size_t ri = 0; ri < n_rules; ri++) {
const auto & rule = rules[ri];
// Split rule into alternates
std::vector<std::pair<size_t, size_t>> alt_ranges;
size_t alt_start = 0;
for (size_t j = 0; j < rule.size(); j++) {
if (rule[j].type == LLAMA_GRETYPE_ALT || rule[j].type == LLAMA_GRETYPE_END) {
alt_ranges.push_back({alt_start, j});
alt_start = j + 1;
}
}
if (alt_ranges.size() < 4) {
continue;
}
// Resolve each alternate: if it's a single RULE_REF, follow it to get the
// actual element sequence from the referenced rule.
std::vector<alt_info> resolved;
resolved.reserve(alt_ranges.size());
bool all_single_refs = true;
for (const auto & [astart, aend] : alt_ranges) {
size_t len = aend - astart;
// Resolving references in generated suffixes can expand recursive rules forever.
if (ri < n_rules_original && len == 1 && rule[astart].type == LLAMA_GRETYPE_RULE_REF) {
size_t ref_id = static_cast<size_t>(rule[astart].value);
if (ref_id < rules.size()) {
size_t ref_end = 0;
for (size_t k = 0; k < rules[ref_id].size(); k++) {
if (rules[ref_id][k].type == LLAMA_GRETYPE_ALT ||
rules[ref_id][k].type == LLAMA_GRETYPE_END) {
ref_end = k;
break;
}
}
bool single_alt = (rules[ref_id][ref_end].type == LLAMA_GRETYPE_END);
if (single_alt) {
resolved.push_back({ref_id, 0, ref_end});
continue;
}
}
}
all_single_refs = false;
resolved.push_back({ri, astart, aend});
}
if (!all_single_refs) {
resolved.clear();
for (const auto & [astart, aend] : alt_ranges) {
resolved.push_back({ri, astart, aend});
}
}
size_t prefix_len = 0;
bool prefix_match = true;
while (prefix_match) {
const auto & first = resolved[0];
if (first.start + prefix_len >= first.end) {
break;
}
const auto & ref_elem = rules[first.rule_idx][first.start + prefix_len];
for (size_t ai = 1; ai < resolved.size(); ai++) {
const auto & alt = resolved[ai];
if (alt.start + prefix_len >= alt.end ||
!elem_eq(rules[alt.rule_idx][alt.start + prefix_len], ref_elem)) {
prefix_match = false;
break;
}
}
if (prefix_match) {
prefix_len++;
}
}
for (const auto & alt : resolved) {
prefix_len = trim_prefix(rules[alt.rule_idx], alt.start, prefix_len);
}
if (prefix_len == 0) {
continue;
}
uint32_t suffix_choice_id = static_cast<uint32_t>(rules.size());
llama_grammar_rule suffix_choice;
for (size_t ai = 0; ai < resolved.size(); ai++) {
if (ai > 0) {
suffix_choice.push_back({LLAMA_GRETYPE_ALT, 0});
}
const auto & alt = resolved[ai];
for (size_t j = alt.start + prefix_len; j < alt.end; j++) {
suffix_choice.push_back(rules[alt.rule_idx][j]);
}
}
suffix_choice.push_back({LLAMA_GRETYPE_END, 0});
llama_grammar_rule new_rule;
const auto & first = resolved[0];
for (size_t j = 0; j < prefix_len; j++) {
new_rule.push_back(rules[first.rule_idx][first.start + j]);
}
new_rule.push_back({LLAMA_GRETYPE_RULE_REF, suffix_choice_id});
new_rule.push_back({LLAMA_GRETYPE_END, 0});
rules.push_back(std::move(suffix_choice));
rules[ri] = std::move(new_rule);
changed = true;
}
}
}
static bool llama_grammar_detect_left_recursion(
const llama_grammar_rules & rules,
size_t rule_index,
@@ -1006,7 +1160,7 @@ static bool llama_grammar_detect_left_recursion(
}
const llama_grammar_rules & llama_grammar_get_rules(const struct llama_grammar * grammar) {
return grammar->rules;
return *grammar->rules;
}
llama_grammar_stacks & llama_grammar_get_stacks(struct llama_grammar * grammar) {
@@ -1035,7 +1189,7 @@ static void llama_grammar_accept_chr(
if (!llama_grammar_is_end_of_sequence(match.second)) {
new_stack.push_back(match.second);
}
llama_grammar_advance_stack(grammar.rules, new_stack, new_stacks);
llama_grammar_advance_stack(*grammar.rules, new_stack, new_stacks);
}
}
@@ -1221,12 +1375,16 @@ struct llama_grammar * llama_grammar_init_impl(
}
}
// Check for left recursion
std::vector<bool> rules_visited(n_rules);
std::vector<bool> rules_in_progress(n_rules);
std::vector<bool> rules_may_be_empty(n_rules);
// Left-factor rules to reduce redundant parallel stacks
llama_grammar_left_factor(vec_rules);
// Check for left recursion (use post-factoring rule count)
const size_t n_rules_total = vec_rules.size();
std::vector<bool> rules_visited(n_rules_total);
std::vector<bool> rules_in_progress(n_rules_total);
std::vector<bool> rules_may_be_empty(n_rules_total);
try {
for (size_t i = 0; i < n_rules; i++) {
for (size_t i = 0; i < n_rules_total; i++) {
if (rules_visited[i]) {
continue;
}
@@ -1267,7 +1425,7 @@ struct llama_grammar * llama_grammar_init_impl(
// then the pointers would be invalidated when the local vec_rules goes out of scope.
return new llama_grammar {
vocab,
std::move(vec_rules),
std::make_shared<const llama_grammar_rules>(std::move(vec_rules)),
std::move(stacks),
/* .partial_utf8 = */ {},
/* .lazy = */ false,
@@ -1319,12 +1477,16 @@ struct llama_grammar * llama_grammar_init_impl(
vec_rules[i].push_back({LLAMA_GRETYPE_END, 0});
}
// Check for left recursion
std::vector<bool> rules_visited(n_rules);
std::vector<bool> rules_in_progress(n_rules);
std::vector<bool> rules_may_be_empty(n_rules);
// Left-factor rules to reduce redundant parallel stacks
llama_grammar_left_factor(vec_rules);
// Check for left recursion (use post-factoring rule count)
const size_t n_rules_total = vec_rules.size();
std::vector<bool> rules_visited(n_rules_total);
std::vector<bool> rules_in_progress(n_rules_total);
std::vector<bool> rules_may_be_empty(n_rules_total);
try {
for (size_t i = 0; i < n_rules; i++) {
for (size_t i = 0; i < n_rules_total; i++) {
if (rules_visited[i]) {
continue;
}
@@ -1378,7 +1540,7 @@ struct llama_grammar * llama_grammar_init_impl(
// then the pointers would be invalidated when the local vec_rules goes out of scope.
return new llama_grammar {
vocab,
std::move(vec_rules),
std::make_shared<const llama_grammar_rules>(std::move(vec_rules)),
std::move(stacks),
/* .partial_utf8 = */ {},
/* .lazy = */ lazy,
@@ -1399,33 +1561,7 @@ void llama_grammar_free_impl(struct llama_grammar * grammar) {
}
struct llama_grammar * llama_grammar_clone_impl(const struct llama_grammar & grammar) {
auto * result = new llama_grammar {
grammar.vocab,
grammar.rules,
grammar.stacks,
grammar.partial_utf8,
grammar.lazy,
grammar.awaiting_trigger,
grammar.trigger_buffer,
grammar.trigger_buffer_positions,
grammar.trigger_tokens,
grammar.trigger_patterns,
};
// redirect elements in stacks to point to new rules
for (size_t is = 0; is < result->stacks.size(); is++) {
for (size_t ie = 0; ie < result->stacks[is].size(); ie++) {
for (size_t ir0 = 0; ir0 < grammar.rules.size(); ir0++) {
for (size_t ir1 = 0; ir1 < grammar.rules[ir0].size(); ir1++) {
if (grammar.stacks[is][ie] == &grammar.rules[ir0][ir1]) {
result->stacks[is][ie] = &result->rules[ir0][ir1];
}
}
}
}
}
return result;
return new llama_grammar(grammar);
}
void llama_grammar_apply_impl(const struct llama_grammar & grammar, llama_token_data_array * cur_p) {
@@ -1465,7 +1601,7 @@ void llama_grammar_apply_impl(const struct llama_grammar & grammar, llama_token_
}
}
const auto rejects = llama_grammar_reject_candidates(grammar.rules, grammar.stacks, candidates_grammar);
const auto rejects = llama_grammar_reject_candidates(*grammar.rules, grammar.stacks, candidates_grammar);
for (const auto & reject : rejects) {
cur_p->data[reject.index].logit = -INFINITY;
}
@@ -1566,7 +1702,7 @@ void llama_grammar_accept_token(struct llama_grammar & grammar, llama_token toke
if (!llama_grammar_is_end_of_sequence(pos + 1)) {
new_stack.push_back(pos + 1);
}
llama_grammar_advance_stack(grammar.rules, new_stack, stacks_new);
llama_grammar_advance_stack(*grammar.rules, new_stack, stacks_new);
}
} else {
llama_grammar_stacks current_stacks = {stack};
+3 -2
View File
@@ -3,6 +3,7 @@
#include "llama.h"
#include <map>
#include <memory>
#include <regex>
#include <string>
#include <vector>
@@ -132,8 +133,8 @@ struct llama_grammar {
// note: allow null vocab for testing (not great)
const llama_vocab * vocab;
const llama_grammar_rules rules; // TODO: shared ptr
llama_grammar_stacks stacks;
const std::shared_ptr<const llama_grammar_rules> rules;
llama_grammar_stacks stacks;
// buffer for partially generated UTF-8 sequence from accepted tokens
llama_partial_utf8 partial_utf8;
+26
View File
@@ -193,6 +193,19 @@ static void test_schema(const std::string & test_desc, const std::string & schem
}
static void test_simple_grammar() {
test_grammar(
"left factor leading empty suffix",
"root ::= \"ab\" | \"abc\" | \"abd\" | \"abe\"",
{"ab", "abc", "abd", "abe"}, {"", "a", "abb", "abcc"});
test_grammar(
"left factor middle empty suffix",
"root ::= \"abc\" | \"ab\" | \"abd\" | \"abe\"",
{"ab", "abc", "abd", "abe"}, {"", "a", "abb", "abcc"});
test_grammar(
"left factor recursive references",
"root ::= \"ok\" | choice\nchoice ::= a | b | c | d\na ::= \"x\" a\nb ::= \"x\" b\nc ::= \"x\" c\nd ::= \"x\" d",
{"ok"}, {"", "x", "xx", "okx"});
test_schema(
"min 0",
R"""({
@@ -623,6 +636,19 @@ static void test_special_chars() {
"🔵🟠abc🟠🔵"
}
);
test_grammar(
"skip left factoring inside character classes",
"root ::= [ab] \"x\" | [ac] \"x\" | [ad] \"x\" | [ae] \"x\"",
{"ax", "bx", "cx", "dx", "ex"}, {"", "a", "fx", "axx"});
test_grammar(
"skip left factoring inside character ranges",
"root ::= [a-b] \"x\" | [a-c] \"x\" | [a-d] \"x\" | [a-e] \"x\"",
{"ax", "bx", "cx", "dx", "ex"}, {"", "a", "fx", "axx"});
test_grammar(
"left factor referenced character alternatives",
"root ::= a | b | c | d\na ::= \"p\" [a] \"x\"\nb ::= \"p\" [ab] \"x\"\nc ::= \"p\" [ac] \"x\"\nd ::= \"p\" [ad] \"x\"",
{"pax", "pbx", "pcx", "pdx"}, {"", "px", "pex", "paxx"});
}
static void test_quantifiers() {
+3 -5
View File
@@ -1,8 +1,6 @@
-r ../../requirements/requirements-convert_legacy_llama.txt
--extra-index-url https://download.pytorch.org/whl/cpu
pillow~=12.3.0
pillow~=11.3.0
torch==2.11.0; python_version < "3.15" # check_requirements: ignore "=="
torch==2.15.0; python_version >= "3.15" # check_requirements: ignore "=="
torchvision==0.26.0; platform_machine != "s390x" and python_version < "3.15" # check_requirements: ignore "=="
torchvision==0.29.2; platform_machine != "s390x" and python_version >= "3.15" # check_requirements: ignore "=="
torch==2.11.0 # check_requirements: ignore "=="
torchvision==0.26.0; platform_machine != "s390x" # check_requirements: ignore "=="
+1 -1
View File
@@ -2,7 +2,7 @@ pytest~=8.3.3
pytest-xdist~=3.6
filelock~=3.16
numpy~=2.2.6; python_version < "3.14"
numpy~=2.5.4; python_version >= "3.14"
numpy~=2.4.6; python_version >= "3.14"
openai~=2.14.0
prometheus-client~=0.20.0
requests~=2.32.3