-
Notifications
You must be signed in to change notification settings - Fork 11.2k
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
Add left recursion check: quit early instead of going into an infinite loop #7083
Conversation
llama.cpp
Outdated
LLAMA_LEFT_REC_FOUND_CYCLE = 3, | ||
}; | ||
|
||
static void detect_left_recursion( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor formatting note -- internal functions and structures such as these should go in the section marked grammar - internal
.
Also, because this is a grammar-specific function, it should probably be renamed llama_grammar_detect_left_recursion
Nice work, @nuchi ! Very sorry for missing this when you first opened it -- I have adjusted my notification settings and hopefully I won't miss such things in the future. This looks really good. I especially like your extensions of the integration tests -- well done! Reading your code, I was initially worried that it might falsely flag examples of non-left recursion, but you handled that perfectly. I like your approach of checking for left recursion as part of the grammar initialization. My first approach was to detect left recursion only at inference time (inside of llama_grammar_advance_stack), but that is non-deterministic -- because if a user has left recursion buried deep in their grammar, it's possible one could generate text that doesn't hit it -- but your check is exhaustive, which I think is better than how I was going to approach it. Looking at the code, I think it can be simplified quite a bit. For instance, you're doing a full walk through the tree every time, but there is possibility to have an early exit in the case of finding recursion. Also, the custom enumeration is cool (and very thorough!), but might be a bit overkill for our needs. All we need to do is track lists of the rules references that are visited in each branch, and check to see if they have been visited before. I took your base code and stripped it down a bit:
And then this would be called from
This is not new code -- I only adapted what you already had here and tried to reduce it to your bare essentials. Overall I love your approach, and it's much better than the direction I was originally taking. Well done! |
tests/test-grammar-integration.cpp
Outdated
static bool test_build_grammar_fails(const std::string & grammar_str) { | ||
bool grammar_fails = false; | ||
try { | ||
build_grammar(grammar_str); | ||
fprintf(stderr, "❌ Expected build failure, but succeeded: %s\n", grammar_str.c_str()); | ||
} catch (const std::exception & err) { | ||
grammar_fails = true; | ||
fprintf(stdout, "✅︎\n"); | ||
} | ||
return grammar_fails; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor formatting adjustment so that we print the grammar on success as well.
static bool test_build_grammar_fails(const std::string & grammar_str) { | |
bool grammar_fails = false; | |
try { | |
build_grammar(grammar_str); | |
fprintf(stderr, "❌ Expected build failure, but succeeded: %s\n", grammar_str.c_str()); | |
} catch (const std::exception & err) { | |
grammar_fails = true; | |
fprintf(stdout, "✅︎\n"); | |
} | |
return grammar_fails; | |
} | |
static bool test_build_grammar_fails(const std::string & grammar_str) { | |
fprintf(stderr, "⚫ Testing failure for grammar: %s\n", grammar_str.c_str()); | |
bool grammar_fails = false; | |
try { | |
build_grammar(grammar_str); | |
fprintf(stderr, " ❌ Expected build failure, but succeeded\n"); | |
} catch (const std::exception & err) { | |
grammar_fails = true; | |
fprintf(stdout, " ✅︎\n"); | |
} | |
return grammar_fails; | |
} |
I especially like how you extended the integration tests here. Super well done! |
Great suggestions, thank you, good idea to ditch the enum. I'll incorporate them with maybe some minor changes — I also just realized I missed an edge case where the leftmost nonterminal(s) might be empty (in which case I also need to recurse into the next-leftmost nonterminal).
Sorry about that and probably my fault — I edited your @ into my initial post, so maybe that's why it didn't ping you. Finally I bring up that it's also a possibility to return nullptr and leave handling to the caller. That'd be messy to do right now since callers aren't expecting it, but also it might be preferable in the case of e.g. a server accepting grammars over the wire where we don't want the server to fall over on a bad request. |
Oh nice! Were you able to build this into another test case?
That might have contributed -- I was also being e-mailed on every PR, which is cool sometimes, but it also added to a bit too much noise, so now I'm back down to participating and mentions. :)
I agree -- that feels cleaner. Along those same lines, you might consider moving this into Only awkward bit with that implementation is that you wouldn't have the rules split up already into those nice vectors, so it might be a bit more pointer math at that stage in the processing, but it's something to consider. |
…internal" section, add handling for edge case where a leftmost nonterminal may be empty
Pushed an update but I still intend to take a look at the interface and maybe return nullptr. I may also look at removing some code repetition. |
Actually, I might leave it as is for now if that looks good to you. I have some ideas about interface changes but I think that'd be a larger change that's decoupled from this one. As of now, the changes here are self-contained and strictly improvements to the status quo, in that the thrown exception here is what would have been a stack overflow or OOM in the status quo. |
Yes, I think that's a fair way to approach it. Especially with the server (that can accept dynamic grammars as part of the query request), I don't think that a bad grammar should be able to crash the server -- it should probably gracefully return an error. That said, such functionality can be added later, and as you say -- everything here is an incremental improvement from the status quo. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
@nuchi added tests/test-grammar-integration.cpp and the CI approved. @HanClinto and @nuchi both agrees that this PR is good enough as a starting point for further incremental improvements. At a glance the reviewed code does not have any obvious issues and there is already back and forth reviews. Safe to merge. |
|
Actually scratch that. The commit ahead showed that this particular error did not happen again. It's a timeout so must be a fluke. Wonder if I can just try kicking off that test again. edit: CI all passes. So definitely a fluke |
…e loop (ggml-org#7083) * Add left recursion check: quit early instead of going into an infinite loop * Remove custom enum, rename left recursion check and move to "grammar internal" section, add handling for edge case where a leftmost nonterminal may be empty * Remove unnecessary declaration
Fixes #6492
Adds a check to see if the grammar has left recursion, and quits instead of going into an infinite loop.
I'm not sure if this is the best place for it, happy to move it around if not.
attn: @HanClinto