- llama.cpp PR #1773 एक ऐसी API जोड़ता है जो context-free grammar के जरिए sampling candidates को सीमित करती है, ताकि generated output तय फ़ॉर्मैट से बाहर न जाए
- API 32-bit code point आधारित grammar data structure लेती है,
llama_sample_grammarसे candidate tokens को filter करती है, फिरllama_grammar_accept_tokenसे चुने गए token को grammar state में लागू करती है mainमें--grammarऔर--grammar-filearguments जोड़े गए हैं, जिनसे extended BNF फ़ॉर्म की grammar input की जा सकती है; उदाहरण के तौर पर Chess, arithmetic expressions, JSON, और Japanese character range generation दिखाए गए हैं- tests M2 Max और 30B Q4_0 मॉडल पर किए गए; grammar लागू होने पर output chess notation, arithmetic expressions, JSON, और Japanese character ranges तक सीमित रहा, जबकि grammar के बिना prompt से असंबंधित सामान्य text या code generate हुआ
- चर्चा में grammar file support, खाली पंक्तियाँ और comments handling, parser को API में शामिल करने या न करने, GPU-based logit masking, fixed token batch processing, और performance overhead जैसी व्यावहारिक उपयोग की सीमाएँ मुख्य रूप से उठाई गईं
PR के मुख्य बदलाव
- यह llama.cpp में grammar-based sampling जोड़ने वाला PR है
- पहले के काम के रूप में #1397 और grantslatton का CFG काम संदर्भ के तौर पर लिया गया है
- नई API serialized context-free grammar लेकर sampling को guide और constrain करती है
mainमें generation grammar तय करने के लिए BNF-जैसे grammar examples जोड़े गए हैं
Grammar API की संरचना
llamaAPI 32-bit code points के लिए context-free grammar data structure लेती है- grammar element types rule end, alternative start, rule reference, character, character range upper bound, और alternate character addition को व्यक्त करते हैं
- initialization function यह जानकारी लेती है
- rule array
- rules की संख्या
- start rule index
llama_grammar_elementमेंtypeऔरvalueहोते हैं, औरvalueUnicode code point या rule ID के रूप में इस्तेमाल होता है
Sampling का तरीका
- grammar sampling code एक nondeterministic pushdown automaton को model करता है
- संभावित parsing states दिखाने के लिए N stacks बनाए रखे जाते हैं
- token sampling दो चरणों में काम करती है
llama_sample_grammarcandidate tokens में से सिर्फ़ वे candidates छोड़ता है जो किसी parsing stack से मेल खाते होंllama_grammar_accept_tokenचुने गए token को grammar state में जोड़ता है
main में grammar input फीचर
mainमें--grammarऔर--grammar-filearguments जोड़े गए हैं- दोनों arguments सरल extended BNF grammar लेकर generation results को सीमित करते हैं
- grammar parser
examples/grammar-parser.{h,cpp}में implement किया गया है - supported grammar features में character ranges, grouping, और repetition operators शामिल हैं
rootrule grammar के starting point की पहचान करता है- बाद के updates में grammar file support, examples, shell-style comments, rules के बीच खाली पंक्तियाँ, और parenthesized groups के भीतर line breaks की अनुमति जोड़ी गई
Test उदाहरण
- test environment M2 Max, 30B मॉडल के रूप में दिया गया है
- Chess उदाहरण
--grammar-file grammars/chess.gbnfइस्तेमाल करने पर1. e4 e5,2. Nf3 Nc6जैसी chess notation generate होती है- grammar के बिना वही prompt चलाने पर Sir Thomas Gresham से जुड़ा सामान्य वाक्य generate हुआ
- Arithmetic expression उदाहरण
- inline grammar से
expr "=" ws num "\n"फ़ॉर्म को अनिवार्य किया गया - result
10 *a*1 +b*2 =640जैसी arithmetic expression shape तक सीमित रहा - grammar के बिना Go code snippet generate हुआ
- inline grammar से
- JSON उदाहरण
grammars/json.gbnfइस्तेमाल करने पर{ "fullName": ..., "address": ... }जैसी JSON structure generate हुई- grammar के बिना self-introduction जैसी गद्य शैली का output बना
- Japanese उदाहरण
grammars/japanese.gbnfhiragana, katakana, punctuation, और CJK ranges को allow करता है- grammar लागू होने पर Japanese characters आधारित सूची generate हुई
- grammar के बिना English step list generate हुई
Review और design चर्चा
- grammar को file के रूप में लेने का सुझाव आया था, और बाद में grammar file support तथा examples जोड़े गए
- शुरुआती उपयोग के दौरान
--prompt-cacheconflict और grammar में खाली पंक्तियों के कारण crash report हुए - खाली पंक्तियाँ और comments support, तथा parenthesized groups के भीतर line breaks की अनुमति बाद के commits में शामिल की गई
- parser को llama.cpp API में शामिल किया जाए या नहीं, इस पर चर्चा हुई
- अगर parser API के बाहर हो, तो downstream users को feature support के लिए parser कॉपी करना पड़ेगा, ऐसी चिंता जताई गई
- लेखक ने
llama.cppमें बदलाव कम रखने की कोशिश की, लेकिन parser को API में रखना अधिक सुविधाजनक है, इस बात से सहमति जताई
- यह सुझाव भी आया कि
llama_grammarअगर binary grammar की copy अपने पास रखे, तो user को दी गई copy की lifetime बनाए रखने की ज़रूरत न पड़े
Performance और optimization चर्चा
- अगर अगले
N > 1tokens grammar से uniquely तय हो रहे हों, तब भी अभी उन्हें एक-एक करके sample किया जाता है, ऐसा समझा गया - कई fixed tokens को batch evaluation करने से ऐसे मामलों में inference speed काफ़ी बढ़ सकती है, ऐसी चर्चा हुई
- लेखक ने जवाब दिया कि tokens का evaluation फिर भी करना पड़ता है, और bottleneck evaluation side पर दिखता है; string-level batch evaluation optimization point हो सकता है
- GPU पर grammar को state-transition tensor में compile करने या पूरे token set के लिए GPU logit masking करने के विचार भी सामने आए
torch-grammarका उल्लेख पूरे token set पर GPU में grammar-enforced logit masking करने वाले समान approach के रूप में किया गया
Performance observations
- लेखक ने बताया कि उन्होंने सिर्फ़ CPU inference आज़माया और उनकी कोशिशों की सीमा में performance impact बहुत बड़ा नहीं था
- M2 Max पर देखे गए आँकड़े
- unconstrained sampling लगभग 0.5ms/token
- grammar-applied sampling लगभग 6ms/token
- 13B Q4_K का token evaluation लगभग 70ms/token
- एक अन्य user ने 13B पर लगभग 20T/s से 13T/s तक गिरावट का मामला बताया
- लेखक ने कहा कि सामान्यतः grammar overhead लगभग 5ms/token दिखता है, लेकिन कुछ grammars में असर बड़ा था और pathological cases संभव हैं
संबंधित विस्तार चर्चा
- JSON Schema input को BNF में बदलकर इस्तेमाल करने की दिशा का उल्लेख हुआ
- लेखक ने बताया कि उनके पास एक local branch है जो jsonformer README example के अनुरूप काम करती है, और एक Python script JSON Schema के अनुरूप JSON BNF generate करती है
- बाद में
examples : generate JSON according to schema #1887का उल्लेख इस PR से जुड़कर हुआ - जवाब में कहा गया कि यह approach model modifications से स्वतंत्र है और fine-tuned models के साथ भी इस्तेमाल की जा सकती है
- whisper.cpp में भी grammar-based sampling जोड़ने वाला एक अलग PR उल्लेखित हुआ
1 टिप्पणियां
Hacker News टिप्पणियाँ
मेरी समझ में यह ऐसे काम करता है। language model, prompt को शुरुआती बिंदु बनाकर एक-एक token आउटपुट करता है, और LLM के साथ बातचीत को भी ऐसे देखा जा सकता है कि user token sequence देता है, model कुछ generate करता है, फिर user आगे जोड़ देता है
यह grammar तकनीक tokens पर कहीं ज्यादा बारीक control देती है। उदाहरण के लिए, अगर आप
Give me the address of the White House as JSON: {"street": "तक दें, तो LLM1600 Pennsylvania Ave NW"लौटाएगा, और जैसे ही closing quote दिखे, user", "City": "inject करकेWashington, DC"पा सकता हैलेकिन क्योंकि यह grammar-based है, यह सिर्फ JSON ही नहीं, उससे कहीं ज्यादा काम कर सकता है। पहले Twitter पर देखा एक सुझाव भी अच्छा था: OpenAI अगर deterministic context-free grammar को API argument के रूप में लेने लगे, या उससे भी आगे जाकर sampler ही बने हुए छोटे WASM binary को कुछ KB में लेकर कुछ MB memory में चलाए, तो LLM की क्षमताएं काफी बढ़ सकती हैं
https://twitter.com/grantslatton/status/1637692033115762688
temperaturesetting यह adjust करती है कि top-ranked न होने वाले tokens चुने जाने की कितनी संभावना हो, जिससे repetitive output कम होता है। LLM को grammar follow करने के लिए force करना आम तौर पर token selection से पहले list को filter करने जैसा है, और फिर भी temperature जिस randomness को control करता है, वह बच सकती हैऔर advanced feature के तौर पर, जब AI अटक जाए और valid output न बना सके, तो backtracking allow करने का तरीका भी है
grammar-based sampling PR का core यह है कि llama.cpp grammar का इस्तेमाल करके अगले output token को possible tokens के सीमित set तक narrow कर देता है
यह Outlines library के एक developer ने लिखा है, और Outlines भी एक अच्छी LLM workflow library है
request के हिस्से के रूप में DCFG rules या compiled version भेज भी दें, तो यह token prediction के तरीके को fundamentally कैसे बदलता है, समझ नहीं आता। अगर model ऐसी चीज predict करे जो मांगी गई grammar से match नहीं करती, तो क्या मतलब है कि match होने तक prompt फिर से देते रहें?
यह बात ध्यान में रखनी चाहिए कि यह model द्वारा generate किए जाने वाले text पर सिर्फ grammatical constraints लगाता है, content alignment सच में ठीक नहीं करता। जब server से well-formatted JSON निकलना guarantee करना हो, तब यह उपयोगी है, लेकिन मौजूदा language generation की कई alignment समस्याएं शायद इससे solve नहीं होंगी
उदाहरण के लिए, मौजूदा Llama या GPT अक्सर Markdown code block labels गलत लगा देते हैं। grammar-based sampling से label लगाना force किया जा सकता है, लेकिन label सही है या नहीं, यह context-dependent है इसलिए force नहीं किया जा सकता। कोई नया domain-specific language बनाने के बाद, उस language के हिसाब से align किए बिना अच्छे output की उम्मीद करना भी मुश्किल है
उदाहरण के लिए, जरूरत के हिसाब से Markdown के लिए LoRA और Markdown grammar file को hot-swap करने वाला system कल्पना किया जा सकता है
यह मुझे वाकई पसंद आया। पहले मैंने Constrained Text Generation Studio बनाया था(https://github.com/Hellisotherpeople/Constrained-Text-Genera...), और इससे जुड़ा paper COLING 2022 में भी छपा था(https://paperswithcode.com/paper/most-language-models-can-be...)
फिर भी मुझे हमेशा लगा कि ऐसे तरीके या इस paper में listed related ideas ही आगे का रास्ता हैं: https://arxiv.org/abs/2306.03081
अब सोचना होगा कि syllable count या syntactic rules जैसी चीजें enforce करने वाली grammar कैसे बनाई जा सकती है। मौजूदा LLM tokenization की वजह से इस तरह के tasks में बहुत खराब हैं
PyTorch के लिए भी इसे implement किया है: https://github.com/Shopify/torch-grammar. इसे इस्तेमाल करने वाला text-generation-inference का hacked version भी है, जरूरत हो तो share कर सकता हूं
multiple-choice string enums, यानी असल में dropdown जैसे cases में, final choice करते समय greedy algorithm के बजाय logits दिए हुए state की पूरी joint probability consider करने पर क्या यह बेहतर काम करेगा, यह सोच रहा हूं
जब starting token list के कई items में shared हो, तो सबसे common starting token वाली choice के बजाय सही choice को prefer किया जा सकता है। बेशक, logits के कुछ हिस्से 0 हो जाने के बाद probabilities adjust करनी होंगी, तभी यह सच में meaningful होगा
इस grammar “library” को format example के तौर पर quote किया गया था: https://github.com/antlr/grammars-v4
इसमें assembly और C++ से लेकर GLSL, scripting languages, arithmetic, games, freedesktop shortcuts, LLVM IR, Verilog जैसे असामान्य formats तक सब कुछ है
क्या कोई आसान भाषा में समझा सकता है? LLMs के बारे में कुछ हद तक पता है, लेकिन Georgi यहां क्या कर रहे हैं और कुछ लोग इसे लेकर इतने excited क्यों हैं, यह ठीक से समझ नहीं आ रहा
ChatGPT जैसे programs उस probability vector को “interpret” करके top tokens में से एक चुनते हैं, यानी sample करके text बनाते हैं। लेकिन यह तरीका कभी-कभी बहुत flexible होता है, इसलिए JSON output चाहने पर भी JSON grammar से मेल न खाने वाला token चुनकर गलत JSON बना सकता है
LLM को JSON जैसी चीज़ generate करने के लिए “force” करने का तरीका sampling process को बदलना है। कोई भी top token चुनने के बजाय, पहले केवल वे tokens बचाए जाते हैं जो JSON grammar से मेल खाते हैं, फिर उसी subset से top token sample किया जाता है
LLM text को token-by-token generate करता है। पहले एक विशाल neural network सभी possible tokens को probabilities assign करता है, फिर sampling procedure उन probabilities का इस्तेमाल करके एक token चुनता है, और यह process repeat होती है
Sampling procedure neural network नहीं है, इसलिए इसे कई तरीकों से बदला जा सकता है। हमेशा highest probability वाला token चुनने वाली greedy sampling भी possible है, लेकिन आमतौर पर probability-weighted random selection बेहतर होती है। इससे diversity आती है और loop में फंसने की संभावना भी घटती है। लेकिन non-zero probability वाला कोई भी token चुना जा सकता है, इसलिए गलत JSON आ सकता है। यह PR grammar के हिसाब से invalid सभी tokens की probability 0 कर देता है ताकि वे चुने न जा सकें
Sampling process में और भी दिलचस्प modifications संभव हैं। Token एक-एक करके sample करते हुए dead end में जा सकते हैं जहां कोई विकल्प न बचे, इसलिए backtracking allow की जा सकती है। आगे बढ़कर, हर step पर कई choices consider करके possible outputs का tree बनाया जा सकता है, और अंत में highest overall probability वाला path चुना जा सकता है। सभी choices consider करने पर possible tokens की संख्या जितनी branching वाला full tree बनेगा, इसलिए यह exponentially बढ़ेगा; इसलिए हर step पर top, मान लें, 5 paths ही रखकर pruning की जा सकती है। यही beam search है। Probability generate करने वाले neural network को run करने की cost बहुत ज्यादा होती है, इसलिए cost 5 गुना बढ़ना भारी पड़ता है; इसी वजह से LLMs में आमतौर पर इसका इस्तेमाल नहीं होता, लेकिन यह संभव है और results भी कुछ हद तक बेहतर होते हैं। Chess engines की तरह Monte Carlo tree search इस्तेमाल करने के बारे में भी सोचा जा सकता है
इसलिए response के लिए ऐसी grammar दी जाती है कि पहले एक uppercase letter आए, फिर lowercase letters, colon, space, digits और फिर end हो। तब पहला token खोजते समय केवल उसी pattern से compatible tokens consider होंगे, और बाद में भी अगले pattern से compatible tokens ही आगे consider होंगे
ऐसी grammar इस काम को flexible और useful pattern में बदल देती है
यह दिलचस्प है, इसलिए मैं इसे अपने काम में शामिल करने की कोशिश करूंगा। हालांकि Bitter Lesson के नजरिए से लगता है कि बहुत short term को छोड़कर यह शायद best approach न हो: http://www.incompleteideas.net/IncIdeas/BitterLesson.html
इसी तरह का तरीका इस्तेमाल करने वाला एक project भी है: https://github.com/automorphic-ai/trex
Playground: https://automorphic.ai/playground
end-to-end neural network के दौर में sampling/decoding कैसे की जाती है, इसे समझाने वाले paper या overview की सिफारिश चाहिए। HMM के समय machine translation और speech recognition में decoding कैसे होती थी, यह पता है, जैसे https://en.wikipedia.org/wiki/Viterbi_algorithm या https://en.wikipedia.org/wiki/Beam_search जैसी चीजें।
आजकल ऐसा impression मिलता है कि लोग बस “greedy” तरीके से करते हैं, लेकिन पक्का नहीं है। इस topic पर कोई material recommend कर सके तो अच्छा होगा
https://platform.openai.com/docs/api-reference/completions/c...
बेशक अब हमें पता है कि GPT-4 में mixture of experts architecture है, इसलिए अंदरूनी तौर पर computation को parallelize किया जा रहा है। इसमें presence/frequency penalty terms के जरिए logits को modify करने का तरीका भी शामिल है