Repository navigation
Conversation
…explosion Grammar rejection is recursive over both stacks and character-rule splits. When a grammar has high branching factor (complex JSON schema, tool-call grammars), the same (stack_state, candidate_list) sub-problem can recur hundreds of times within a single sampling step, causing generation speed to drop from ~35 T/s to ~5 T/s or a complete hang. Adds a persistent memoization cache (keyed on stack content + candidate content) to llama_grammar_reject_candidates_for_stack. The cache lives in the llama_grammar struct and persists for the grammar's lifetime, allowing recurring stack states (e.g. repeated array elements, string character loops) to benefit across multiple token generations. Keys are content-based (actual decoded code-point values, not raw struct bytes including pointers), eliminating aliasing hazards across calls when the allocator reuses addresses. Only candidate lists of <= MEMO_CANDIDATES_MAX (8) elements are memoized; larger lists are rare in the recursion hot path and are left uncached. The public 3-arg llama_grammar_reject_candidates_for_stack signature is preserved (tests/test-llama-grammar.cpp continues to compile unchanged). llama_grammar_apply_impl passes grammar.memo_cache to the internal 4-arg reject_candidates path for cross-call persistence. Corrects a TOKEN/TOKEN_NOT cache-miss bug present in the koboldcpp source: the koboldcpp implementation pre-inserted an empty vector via operator[] before the TOKEN/TOKEN_NOT early return, leaving a stale empty result in the cache. This implementation stores the result after computing it, ensuring TOKEN/TOKEN_NOT branches are cached correctly. Adapted from koboldcpp PR LostRuins#1615 (LostRuins/koboldcpp) by Reithan.
|
Hi @Reithan, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
Summary revised with hand-written description following proper template. Sorry for the mix-up. |
|
CI run locally with result: All tests passed.
real 0m2.423s
user 0m7.932s
sys 0m0.190s
```
### ctest_with_model_debug
Runs ctest with model files in debug mode
- status: 127
```
+ LLAMACPP_TEST_MODELFILE=/home/reithan/projects/llama.cpp/tmp/mnt/models/qwen3/0.6B/ggml-model-q4_0.gguf
+ time ctest -C Debug --output-on-failure -L model
./ci/run.sh: line 379: time: command not found
```This should cover all tests relevant to this PR. The |
|
Grammar specific results from CI run were: (.venv)~/projects/llama.cpp$ grep -i "test-llama-grammar" ./tmp/results/*-ctest.log
./tmp/results/ctest_debug-ctest.log: Start 22: test-llama-grammar
./tmp/results/ctest_debug-ctest.log:22/50 Test #22: test-llama-grammar ......................... Passed 0.00 sec
./tmp/results/ctest_release-ctest.log: Start 22: test-llama-grammar
./tmp/results/ctest_release-ctest.log:22/53 Test #22: test-llama-grammar ......................... Passed 0.00 sec |
|
Baseline bench results for regression check: | model | size | params | backend | threads | test | t/s |
| ------------------------------ | ---------: | ---------: | ---------- | ------: | --------------: | -------------------: |
| qwen3 0.6B Q4_0 | 358.21 MiB | 596.05 M | CPU | 4 | pp512 | 404.22 ± 11.98 |
| qwen3 0.6B Q4_0 | 358.21 MiB | 596.05 M | CPU | 4 | tg128 | 93.44 ± 1.31 |
build: 4cbe8b070 (10803)
| model | size | params | backend | threads | test | t/s |
| ------------------------------ | ---------: | ---------: | ---------- | ------: | --------------: | -------------------: |
| qwen3 0.6B Q4_0 | 358.21 MiB | 596.05 M | CPU | 4 | pp512 | 404.49 ± 19.03 |
| qwen3 0.6B Q4_0 | 358.21 MiB | 596.05 M | CPU | 4 | tg128 | 93.03 ± 1.61 |
build: c68fd6ea6 (10805) |
|
Simple standard grammar test shows minor improvement over baseline (baseline shown first, then PR): Generate a JSON object with fields for name, age, and address:{"name": "John", "age": 25, "address": "123 Main Street"}
[end of text]
0.02.177.730 I common_perf_print: sampling time = 98.94 ms
0.02.177.731 I common_perf_print: samplers time = 4.34 ms / 57 tokens
0.02.177.734 I common_perf_print: load time = 1353.43 ms
0.02.177.747 I common_perf_print: prompt eval time = 41.06 ms / 14 tokens ( 2.93 ms per token, 340.98 tokens per second)
0.02.177.748 I common_perf_print: eval time = 466.56 ms / 42 runs ( 11.11 ms per token, 90.02 tokens per second)
0.02.177.748 I common_perf_print: total time = 608.83 ms / 56 tokens
0.02.177.749 I common_perf_print: unaccounted time = 2.28 ms / 0.4 % (total - sampling - prompt eval - eval) / (total)
0.02.177.749 I common_perf_print: graphs reused = 41
...
0.01.495.789 I common_perf_print: sampling time = 93.05 ms
0.01.495.790 I common_perf_print: samplers time = 3.86 ms / 57 tokens
0.01.495.794 I common_perf_print: load time = 692.78 ms
0.01.495.795 I common_perf_print: prompt eval time = 40.78 ms / 14 tokens ( 2.91 ms per token, 343.28 tokens per second)
0.01.495.796 I common_perf_print: eval time = 459.76 ms / 42 runs ( 10.95 ms per token, 91.35 tokens per second)
0.01.495.796 I common_perf_print: total time = 596.09 ms / 56 tokens
0.01.495.797 I common_perf_print: unaccounted time = 2.50 ms / 0.4 % (total - sampling - prompt eval - eval) / (total)
0.01.495.797 I common_perf_print: graphs reused = 41 |
Assisted-by: Claude Sonnet
…g invalid pointers from prior cache use
|
Testing a purely degenerate case shows that the current dedupe strategy doesn't fully cover cases of ambiguous grammar. In cases where multiple options create duplicate alternatives (identical character classes or literals defined separately), this can still cause exponential branch explosion due to distinct rule/body pointers that the existing dedupe can't collapse. Tested with: Result is baseline times out, while PR build gives: 0.02.323.246 I common_perf_print: sampling time = 267.22 ms
0.02.323.248 I common_perf_print: samplers time = 1.94 ms / 31 tokens
0.02.323.251 I common_perf_print: load time = 1543.32 ms
0.02.323.252 I common_perf_print: prompt eval time = 34.06 ms / 11 tokens ( 3.10 ms per token, 322.93 tokens per second)
0.02.323.253 I common_perf_print: eval time = 217.31 ms / 19 runs ( 11.44 ms per token, 87.43 tokens per second)
0.02.323.253 I common_perf_print: total time = 519.93 ms / 30 tokens
0.02.323.254 I common_perf_print: unaccounted time = 1.35 ms / 0.3 % (total - sampling - prompt eval - eval) / (total)
0.02.323.254 I common_perf_print: graphs reused = 18 |
…l necessary checks
…or entire request. avoid use of any transient pointers
Overview
This PR is a collection of changes from PRs that merged to koboldcpp months ago. The purpose of these changes was to remove possible edge cases from schema or grammar constrained inference that caused slowdowns or hangs. The core mechanism here was a hand-tuned memoization cache to avoid runaway branch explosion while parsing the grammar, along with some other basic optimizations around how grammar interacts with other samplers.
This PR only migrates the memoization changes, 2 more PRs branches are prepared to migrate the compaction and pre-cull changes after this is reviewed.
Additional information
The prior related PRs in the koboldcpp repo are:
If fresh benchmark numbers are needed, I will be able to provide them soon.
Requirements