Skip to content
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

tool-call: fix Qwen 2.5 Coder support, add micro benchmarks, support trigger patterns for lazy grammars #12034

Merged
merged 48 commits into from
Mar 5, 2025

Conversation

ochafik
Copy link
Collaborator

@ochafik ochafik commented Feb 22, 2025

TL;DR: fixes tool calling of Qwen 2.5 Coder 0.5B/1.5B/3B/7B/32B... at any temperature

Follow up to #9639

instructions to build this branch
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
git remote add ochafik https://github.com/ochafik/llama.cpp
git fetch ochafik
git checkout ochafik/tool-bench-prod
cmake -B build -DLLAMA_CURL=1
cmake --build build -t llama-server --parallel --config Release
alias llama-server=./build/bin/llama-server
llama-server --jinja -fa -c 0 -hf unsloth/Qwen2.5-Coder-7B-Instruct-128K-GGUF
  • Added support for regex grammar triggers, and respect when they should be matching at the start only (was already declared but not implemented; should avoid spurious triggering when the triggers were defined as wide-catches).
    • In llama.h, deprecating llama_sampler_init_grammar_lazy (which used to take tokens or words) in favour of llama_sampler_init_grammar_lazy_patterns (which takes tokens or full-string regex patterns w/ a group that marks from where the grammar is triggered)
  • Dramatically improved tool calls success rate of Qwen 2.5 Coder (Hermes 2 format) w/ more triggers that match what the models tends to output (esp. at higher temperatures) / looser triggers w/ regular expressions
  • Added scripts/tool_bench.py to evaluate tool call compliance probability of llama-server & ollama on different models, at different temperatures

The following heatmap shows compliance ratio on two super basic tool call tests (hello world & weather tests from examples/server/tests/unit/test_tool_call.py, now shared w/ the bench tool). 3 pairs of columns for llama-server of this PR, baseline llama-server (master) and ollama.

image

qwenc1 5b

export ARGS=( --n 30 --llama-baseline="$(which llama-server)" --temp -1 --temp 0 --temp 0.5 --temp 0.75 --temp 1 --temp 1.5 --temp 2 --temp 5 ) 

./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 Coder 7B Q4_K_M"             --output ../qwenc7b.jsonl   --hf unsloth/Qwen2.5-Coder-7B-Instruct-128K-GGUF:Q4_K_M   --ollama qwen2.5-coder:7b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 Coder 1.5B Q4_K_M"           --output ../qwenc1.5b.jsonl --hf unsloth/Qwen2.5-Coder-1.5B-Instruct-128K-GGUF:Q4_K_M --ollama qwen2.5-coder:1.5b-instruct-q4_K_M

See gist with results for many more models

Notes about results:

  • the failures of llama-server at temp = 2 are model humour / stylistic choice (Sure! You can use the following Python code... instead of tool call)
  • ollama seems to only recognize the tool call format of the template, but models like Qwen 2.5 Coder 7B is quite... creative in its tool call outputs, esp. at higher temperatures.
  • ollama's default temperature seems to be 0.6 (hence why the row w/ @ None kinda fits results of lower rows)
  • The tests may need further tweaking to accept arguably “correct” answers. The framing of the hello world test is questionable, sometimes models just explain how they would write the code.
  • The benchmark tool also supports running test_calc_results which evaluates how well a model follows up on tool results. This seems to have more varied failure modes so it's not evaluated by default.

TODO:

  • Run & share more bench results (esp. other Qwen Coder variants!)
  • Stabilize tests / ci
  • Analyze bench times

Sorry, something went wrong.

Verified

This commit was signed with the committer’s verified signature.
JohnTitor Yuki Okushi
Update llama-grammar.h

update

Update llama-grammar.h

Update common.h

Update common.h

Update sampling.cpp

Update chat.cpp

update test_tool_call.py

Update server.cpp

Update utils.hpp

Update chat.cpp

Update test_tool_call.py

Update fetch_server_test_models.py

Verified

This commit was signed with the committer’s verified signature.
JohnTitor Yuki Okushi

Verified

This commit was signed with the committer’s verified signature.
davidtwco David Wood
…3 8b tool outputs)
@github-actions github-actions bot added script Script related testing Everything test related examples python python script changes server labels Feb 22, 2025
@GuuD
Copy link

GuuD commented Feb 22, 2025

Was Qwen 2.5 Coder even trained for tool use? 🤯

@ochafik
Copy link
Collaborator Author

ochafik commented Feb 23, 2025

Was Qwen 2.5 Coder even trained for tool use? 🤯

@GuuD I guess all models must be to some extent, these days. Their technical report only mentions in passing the fact that BigCodeBench is "primarily aimed at evaluating the ability of tool-use and complex instruction following" and their results on that benchmark look quite decent. But given the variety of outputs the model wraps tool calls in, I doubt they stuck to the syntax used in their jinja template.

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.

Verified

This commit was signed with the committer’s verified signature.
dtolnay David Tolnay

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
…n outputs
Copy link
Collaborator Author

@ochafik ochafik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ochafik
Copy link
Collaborator Author

ochafik commented Mar 5, 2025

In general this LGTM, although some part I'm not understand 100%.

Btw, since the complexity of chat.cpp is growing recently, I think it would be beneficial to have less dependency on json type. The problem is that json type is known to be bad for performance, and also make static analyzing code impossible.

Thanks @ngxson !

I wonder if we could use some of the static json tricks used by alibaba/yalantinglibs (nifty macros that make data structures statically reflectable, for faster & safer serialization / deserialization). But if performance is the main concern I'd start w/ some benchmarks once the tool / mcp things stabilize (incl. streaming), there's larger bits I'd like to optimize / cache (grammar parsing, schema generation...).

Co-authored-by: Georgi Gerganov <[email protected]>
@ggerganov
Copy link
Member

For me the main concerns with json.hpp is the static analysis difficulties (as @ngxson mentioned) and also increased compilation time. I don't have a good feeling how much json affects the performance of applications, but it's second-tier concern in my list compared to the former.

}
}
}
if (params.sampling.grammar_lazy) {
GGML_ASSERT(params.sampling.grammar_trigger_tokens.size() > 0 || params.sampling.grammar_trigger_words.size() > 0);
GGML_ASSERT(!params.sampling.grammar_triggers.empty());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never use GGML_ASSERT inside params_from_json_cmpl as it will crash the whole server and malicious users can DDoS the server easily. Throw an std::runtime_error instead

std::string value;
llama_token token = LLAMA_TOKEN_NULL;

template <class T> T to_json() const;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ok I didn't notice that we cannot include json in this file. Then maybe change it to:

template <class T> T to() const;

Then use with to<json> ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it does read better in the call site, I think naming it just to makes the interface harder to understand to readers, esp. given the unconventional template use (if anything, would name it serialize / deserialize, or provide operator<< / operator>>). Happy to revisit in a follow up / to batch update all the to_json* to something :-)

@@ -559,29 +587,29 @@ static common_chat_msg parse_json_tool_calls(
return result;
}

static common_chat_tool_call process_tool_call(const json & tool_call) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also apply the same to/from<json> pattern that I discussed above for common_chat_tool_call

@ngxson
Copy link
Collaborator

ngxson commented Mar 5, 2025

I wonder if we could use some of the static json tricks used by alibaba/yalantinglibs (nifty macros that make data structures statically reflectable, for faster & safer serialization / deserialization)

IMO this doesn't seem to be beneficial of us, as it implies even more dependency to json (which we currently want to avoid).

Also I'm doubt if it is even faster. The benchmark shown is for struct_pack and not struct_json, I don't think we can go further when parsing/serializing json.

Looking at their example:

struct person {
  std::string name;
  int age;
};
YLT_REFL(person, name, age);

struct_json::to_json(p, str);
struct_json::from_json(p1, str);

Already, this is quite bad for us since sometimes we what to have optional fields or fields with multiple possible values. For example, "prompt" can be string or an array of token in our case, which breaks that "statically reflectable" that you mentioned.

Also, having explicit to_json / from_json seems safer since you can explicitly validate the input data.

But if performance is the main concern I'd start w/ some benchmarks once the tool / mcp things stabilize (incl. streaming), there's larger bits I'd like to optimize / cache (grammar parsing, schema generation...).

The performance lost is not about serializing / deserializing the json, but it comes from the fact that you do need to get something by .at("...") and set by doing json_obj["..."] = .... The reason why this is bad because this set/get operation relies on [un]ordered_map, which has complexity O(log n)

For example, in these 2 versions, json is clearly slower:

json data = {
  {"name", "abc"},
  {"age", 123}
};

std::string salutation = "Hello, " + data.at("name"); // O(log n) each time you use "at"

// versus struct
struct person {
  std::string name;
  int age;
};
person p = person{"abc", 123};

std::string salutation = "Hello, " + p.name; // always O(1)

@ochafik
Copy link
Collaborator Author

ochafik commented Mar 5, 2025

IMO this doesn't seem to be beneficial of us, as it implies even more dependency to json (which we currently want to avoid).
Also I'm doubt if it is even faster. The benchmark shown is for struct_pack and not struct_json, I don't think we can go further when parsing/serializing json.

@ngxson I didn't test this yet tbh, but even without static json metaprogramming tricks (which on paper could unlock some speed) there's definitely room for faster parsing and serialisation, cf. @jart's https://github.com/jart/json.cpp (a serious option to also lower compilation times btw, with a maybe slightly more verbose interface but also maybe less syntax gotchas)

Already, this is quite bad for us since sometimes we what to have optional fields or fields with multiple possible values. For example, "prompt" can be string or an array of token in our case, which breaks that "statically reflectable" that you mentioned.

Yeah we'd need to evaluate feasibility of support std::variant and std::optional to make this work (potentially a cool weekend exploration - deciding on variant branches, if possible, could be interesting - might require extra metadata declaration, and runtime backtracking and/or a static disambiguation strategy - assuming we can do enough constexpr magic).

I think the main value in adding a bit of static typing would be to reduce the surface for misuse. (and also, would move the json dependency entirely in a single generic serialization compilation unit)

is bad because this set/get operation relies on [un]ordered_map, which has complexity O(log n)

Actually it's a hashmap (Edit: my bad, looks like ordered_json uses a vector so it's all O(n) (ouch!), and the default unordered json - which I don't think we use - uses std::map so indeed O(log n))

@ngxson
Copy link
Collaborator

ngxson commented Mar 5, 2025

there's definitely room for faster parsing and serialisation

As said, there are not many places in the code we use parsing / serializing, so just to remind here that it may not be beneficial to optimize this part.

Actually it's a hashmap so average O(1), worst case O(n) (with n very small anyway).

Ok maybe I confused with std::map that uses red-black tree.

But in anyway, that O(1) is still slower than accessing struct member. Don't forget that with a std::map<std::string, ...> you also need to calculate the hash of the string, which depends on the string length. By contrast, accessing class member can be done in just one pointer dereference, and can be even optimized at compile time.

I think the main value in adding a bit of static typing would be to reduce the surface for misuse. (and also, would move the json dependency entirely in a single generic serialization compilation unit)

I'm not sure if I understand this correctly, aren't struct already used for static typing?

@ngxson
Copy link
Collaborator

ngxson commented Mar 5, 2025

static json metaprogramming

Maybe a bit off-topic, IMO doing this with C macro should not be a big problem. The biggest problem is that we try not to use too many macro (or nested macro), as it is impossible to debug with something like gbd. When possible, use template instead.

@ochafik
Copy link
Collaborator Author

ochafik commented Mar 5, 2025

Ok maybe I confused with std::map that uses red-black tree.

@ngxson cf. my update above, the default nlohmann::json (which we mostly don't use) uses an std::map indeed, but nlohmann::unordered_json uses... a vector 😅 (probably not a huge issue given they're all smallish anyway)

I think the main value in adding a bit of static typing would be to reduce the surface for misuse. (and also, would move the json dependency entirely in a single generic serialization compilation unit)

I'm not sure if I understand this correctly, aren't struct already used for static typing?

Yes but not in places like the to_json_oaicompat*, where I've been caught many times by an extraneous pair of curly braces that completely change the meaning of a json array, etc.

static json metaprogramming

Maybe a bit off-topic, IMO doing this with C macro should not be a big problem. The biggest problem is that we try not to use too many macro (or nested macro), as it is impossible to debug with something like gbd. When possible, use template instead.

Absolutely! Those variadic macros give me bad chills haha, and not sure I like the idea of maintaining variadic templates either, but I'm keeping an open mind ;-)

@ngxson
Copy link
Collaborator

ngxson commented Mar 5, 2025

Yes but not in places like the to_json_oaicompat*, where I've been caught many times by an extraneous pair of curly braces that completely change the meaning of a json array, etc.

I see. Yeah in fact the main purpose of to_json_oaicompat* is to convert the struct data into OAI-compat schema. Problem is that, the original schema is in Open API format, so I'm not sure how we can translate it into cpp. For now, we rely mostly on the server pytest scripts to check that.

@ochafik ochafik merged commit 669912d into ggml-org:master Mar 5, 2025
50 checks passed
mglambda pushed a commit to mglambda/llama.cpp that referenced this pull request Mar 8, 2025
…t trigger patterns for lazy grammars (ggml-org#12034)

* sampler: turn lazy grammar trigger words to regexes

* add scripts/tool_bench.sh & .py

* constrain llama json output regardless of function name if matches at beginning

* update relaxed newline space rule in grammar tests

* support add_generation_prompt query parameter (useful for /apply_template)

* Update src/llama-grammar.cpp

Co-authored-by: Georgi Gerganov <[email protected]>

---------

Co-authored-by: Georgi Gerganov <[email protected]>
@codefromthecrypt
Copy link

ran into some snags and listed them here. #12279 one case almost passed. If anyone can take a look, I'd love to get something stable working as I'm demonstrating this in about 10 days.

arthw pushed a commit to arthw/llama.cpp that referenced this pull request Mar 19, 2025
…t trigger patterns for lazy grammars (ggml-org#12034)

* sampler: turn lazy grammar trigger words to regexes

* add scripts/tool_bench.sh & .py

* constrain llama json output regardless of function name if matches at beginning

* update relaxed newline space rule in grammar tests

* support add_generation_prompt query parameter (useful for /apply_template)

* Update src/llama-grammar.cpp

Co-authored-by: Georgi Gerganov <[email protected]>

---------

Co-authored-by: Georgi Gerganov <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
examples python python script changes script Script related server testing Everything test related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants