Repository: ggml-org/llama.cpp
Pull request: llama: fix error on bad grammar

On master passing a bad grammar to either `llama-cli` or the server currently does not result in an error, there is only a print indicating that the grammar could not be parsed and the grammar is otherwise ignored. However, I don't think that this is the intended behavior. The code checks if the return value of `common_sampler_init` is null, it's just that the return value is not null when passing a bad grammar. This PR makes it so that `llama_sampler_init_grammar_impl` returns null when `grammar-str` is not empty but could not be passed, this is then propagated upwards to return null with `common_sampler_init`. When passing a bad grammar to the server the server now returns an error, `llama-cli` terminates with an error message.

I used the following commands for testing:

```bash
export model_name=llama_3-8b && export quantization=q4_0
./build/bin/llama-cli --model models/opt/${model_name}-${quantization}.gguf -ngl 99 --n-predict 1 --prompt "1 2 3 4 " --grammar "root ::= \"6\""
./build/bin/llama-cli --model models/opt/${model_name}-${quantization}.gguf -ngl 99 --n-predict 1 --prompt "1 2 3 4 " --grammar "asdf"
./build/bin/llama-server --model models/opt/${model_name}-${quantization}.gguf --n-gpu-layers 99
curl --request POST --url http://localhost:8080/completion --data '{"prompt": "1 2 3 4 ", "n_predict": 1, "grammar": "root ::= \"6\""}' | python3 -m json.tool
curl --request POST --url http://localhost:8080/completion --data '{"prompt": "1 2 3 4 ", "n_predict": 1, "grammar": "asdf"}' | python3 -m json.tool
```

Changed files:
filename: common/sampling.cpp
patch: @@ -208,6 +208,9 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, co
                                                         trigger_patterns_c.data(), trigger_patterns_c.size(),
                                                         trigger_tokens.data(), trigger_tokens.size())
              :      llama_sampler_init_grammar(vocab, params.grammar.c_str(), "root");
+        if (!grmr) {
+            return nullptr;
+        }
     }
 
     auto * result = new common_sampler {

filename: include/llama.h
patch: @@ -1265,6 +1265,10 @@ extern "C" {
                                float   tau,
                                float   eta);
 
+    /// @details Intializes a GBNF grammar, see grammars/README.md for details.
+    /// @param vocab The vocabulary that this grammar will be used with.
+    /// @param grammar_str The production rules for the grammar, encoded as a string. Returns an empty grammar if empty. Returns NULL if parsing of grammar_str fails.
+    /// @param grammar_root The name of the start symbol for the grammar.
     LLAMA_API struct llama_sampler * llama_sampler_init_grammar(
             const struct llama_vocab * vocab,
                           const char * grammar_str,

filename: src/llama-sampling.cpp
patch: @@ -1477,6 +1477,7 @@ static struct llama_sampler * llama_sampler_grammar_clone(const struct llama_sam
     const auto * ctx = (const llama_sampler_grammar *) smpl->ctx;
 
     auto * result = llama_sampler_init_grammar_impl(ctx->vocab, nullptr, nullptr, false, nullptr, 0, nullptr, 0, nullptr, 0);
+    GGML_ASSERT(result);
 
     // copy the state
     {
@@ -1548,6 +1549,10 @@ static struct llama_sampler * llama_sampler_init_grammar_impl(
             /* .grammar_root = */ grammar_root,
             /* .grammar      = */ llama_grammar_init_impl(vocab, grammar_str, grammar_root, lazy, trigger_patterns, num_trigger_patterns, trigger_tokens, num_trigger_tokens),
         };
+        if (!ctx->grammar) {
+            delete ctx;
+            return nullptr;
+        }
     } else {
         *ctx = {
             /* .vocab        = */ vocab,