कॉन्स्ट्रेन्ड डिकोडिंग: LLM को तेज़ निर्णय मॉडल में बदलें
कॉन्स्ट्रेन्ड डिकोडिंग LLM को तेज़ क्लासिफायर बना देती है। जानें कैसे लॉजिट मास्किंग, कैलिब्रेशन और टेम्परेचर स्केलिंग इन निर्णय मॉडलों को भरोसेमंद बनाते हैं।

यहाँ एक सस्ती लेकिन चुपचाप काम की तरकीब है: अगर आपको सिर्फ़ यह जानना है कि LLM पहला टोकन क्या निकालेगा, तो आप उससे एक मल्टीपल-चॉइस सवाल पूछ सकते हैं और एक ही फ़ॉरवर्ड पास में जवाब पा सकते हैं। कॉन्स्ट्रेन्ड डिकोडिंग — वोकैबुलरी के हर टोकन को मास्क करके सिर्फ़ उन कुछ टोकन को छोड़ देना जिन्हें आप स्वीकार करने को तैयार हैं — किसी जनरेटिव मॉडल को लगभग क्लासिफायर जैसा बना देता है। न JSON पार्स करना, न रिट्राई लूप, न ग्यारह ऑटोरेग्रेसिव स्टेप्स ताकि ग्यारह अक्षर बन सकें। एक पास, एक softmax, और हर विकल्प के साथ उसकी संभावना वाला एक जवाब।
यह विचार "डिसीज़न मॉडल" या "system one" inference जैसे नामों से पहले से चल रहा था, और हाल ही में Hacker News पर एक वॉकथ्रू के ज़रिए फिर चर्चा में आ गया, जिसमें दिखाया गया कि लगभग चालीस लाइन के Python में 1.7B-पैरामीटर वाले Qwen मॉडल से एक कैसे बनाया जाए। कमेंट में मशीन-लर्निंग के दिग्गज, जैसा कि अपेक्षित था, गुस्से में चिल्ला रहे थे: यह तो क्लासिफायर है, ये तो perceptron के ज़माने से हैं। वे सही हैं, और थोड़ा मुद्दे से चूक भी रहे हैं। नई चीज़ कॉन्सेप्ट नहीं है — नई बात यह है कि एक सामान्य-उद्देश्य भाषा मॉडल से, बिना कुछ ट्रेन किए, आपको zero-shot क्लासिफायर मिल जाता है। असली इंजीनियरिंग सवाल यह है कि यह कॉन्स्ट्रेन्ड तरीका कब मॉडल को बस बोलने देने से बेहतर है, और कब यह बढ़ा-चढ़ाकर दिए गए संभावनाओं से चुपचाप आपको धोखा देने लगता है।
LLM से जवाब निकालने के दो तरीके
भाषा मॉडल से बात करने का डिफ़ॉल्ट तरीका जनरेशन है। आप सवाल पूछते हैं, मॉडल एक-एक करके टोकन निकालता है, और बाद में आप जो भी आया उसे पार्स करते हैं। अगर संरचित आउटपुट चाहिए, तो आप एक स्कीमा जोड़ देते हैं: JSON mode, grammar-constrained sampling, outlines-style finite-state machines। ये काम करते हैं, लेकिन मॉडल फिर भी पूरे जवाब में टोकन-दर-टोकन चलता है। एक साधारण मल्टीपल-चॉइस जवाब में भी ग्यारह डिकोडिंग स्टेप लग सकते हैं, और हर स्टेप में अरबों पैरामीटर पर पूरा फ़ॉरवर्ड पास होता है।
दूसरा विकल्प यह है कि उसे कभी चलने ही न दें। प्रॉम्प्ट प्रोसेस होने के बाद, आखिरी पोज़ीशन पर वोकैबुलरी के logits देखें, उनमें से सिर्फ़ आपके विकल्पों के टोकन ID रखें — मान लीजिए "A", "B", "C", "D", "E" — और सिर्फ़ उन्हीं पर softmax लगाएँ। argmax आपका अनुमान है; softmax मान स्कोर हैं। कुल लागत: एक फ़ॉरवर्ड पास, जो वैसे भी prefill में लग रहा था। मूल हिस्सा यह रहा, उस Qwen-आधारित तरीके से अनुकूलित जिसकी आजकल खूब चर्चा है:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Qwen/Qwen3-1.7B"
options = ["A", "B", "C", "D", "E"]
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype="auto", device_map="auto"
)
# First token the model would emit for each option
option_token_ids = [
tokenizer.encode(opt, add_special_tokens=False)[0] for opt in options
]
prompt = (
"What color is the sky?\n"
"A. Red\nB. Blue\nC. Green\nD. Purple\nE. I don't know\nAnswer:"
)
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True,
enable_thinking=False
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
logits = model(**inputs).logits[0, -1]
# Constrained decoding: softmax over only the option tokens
probs = torch.softmax(logits[option_token_ids].float(), dim=-1)
print(options[probs.argmax().item()]) # -> B
बस यही पूरा इंजन है। वोकैबुलरी में 150,000 से ज़्यादा टोकन हैं और हमने फ़ैसले को पाँच नंबरों तक सीमित कर दिया है। जनरेशन के समय sampling temperature के खेल नहीं, कोई पार्सर नहीं जो फेल हो सके, और मॉडल के लिए ऐसा कोई विकल्प गढ़ने का रास्ता नहीं जो सूची में न हो। आउटपुट स्पेस डिज़ाइन से ही बंद है।
यह क्लासिफायर है, और यह ठीक है
चलिए संदेह करने वालों को उनका हक़ दें। हमने जो बनाया है वह एक fixed label set पर discriminative classifier है — यह उन विचारों की संतान है जो 1950 के दशक के Rosenblatt के perceptron से शुरू होकर logistic regression, और डीप लर्निंग युग के हर softmax-हेड वाले neural net तक जाते हैं। थोड़ा गौर से देखें तो कॉन्स्ट्रेन्ड डिकोडिंग बस अंतिम hidden state पर एक linear readout है, जो हमेशा से किसी classification head का काम रहा है। जो ML इंजीनियर सालों से अपनी टीम से बस एक क्लासिफायर ट्रेन करने की गुहार लगा रहा है, उसे इसे "डिसीज़न मॉडल" नाम से दोबारा ब्रांड होते देखकर थोड़ा चिढ़ना बिल्कुल स्वाभाविक है।
लेकिन इतिहास यह भी दिखाता है कि नया संस्करण क्यों मायने रखता है। पुराने क्लासिफायर संकीर्ण थे: आप लेबल वाला डेटा इकट्ठा करते, ट्रेन करते, और आपके पास एक ऐसा मॉडल होता जो एक ही काम जानता था। LLM-आधारित सिस्टम उन कामों को भी हड़पते जा रहे हैं जिनके लिए "सही" क्लासिफायर चाहिए था, इसका कारण zero-shot गुण है — बेस मॉडल दुनिया का इतना ज्ञान पहले ही समेट चुका है कि एक प्रॉम्प्ट ही ट्रेनिंग डेटा बन जाता है। जब मैंने ऐसे सेटअप को CommonsenseQA holdout पर परखा, तो बिना किसी फ़ाइन-ट्यूनिंग के 1.7B मॉडल लगभग 59% accuracy पर पहुँचा, और training split पर एक त्वरित ट्यून के बाद लगभग 62% तक गया। यह state of the art नहीं है, लेकिन इसमें एक दोपहर लगी और मेरा अपना कोई लेबल वाला डेटा नहीं लगा। एक खास तौर पर बना क्लासिफायर शायद बेहतर करे; लेकिन उसके लिए हर बार लेबल बदलने पर एक पाइपलाइन, एक डेटासेट और रीट्रेनिंग की योजना भी चाहिए।
यहाँ पुरानी generative-versus-discriminative बहस से एक उपयोगी ढाँचा मिलता है। Generative classifiers पूरे डिस्ट्रीब्यूशन को मॉडल करते हैं और इसलिए महंगे लेकिन लचीले होते हैं; discriminative classifiers decision boundary को मॉडल करते हैं और इसलिए कुशल लेकिन कठोर होते हैं। कॉन्स्ट्रेन्ड डिकोडिंग एक अजीब मिश्रण है: एक generative मॉडल को inference के समय discriminative सेवा में लगा दिया जाता है। आपको discriminative readout की दक्षता और generative pretraining की व्यापकता दोनों मिलती हैं। यह संयोजन पहले सचमुच उपलब्ध नहीं था, भले ही इसके हिस्से पुराने हों।
जहाँ कॉन्स्ट्रेन्ड डिकोडिंग जीतता है
- Latency. N ऑटोरेग्रेसिव स्टेप्स की जगह एक फ़ॉरवर्ड पास। छोटे मॉडलों पर यही फ़र्क है कि यह 'request path के लिए काफ़ी तेज़' है या 'queue चाहिए'। ब्राउज़र में चलने वाले शुद्ध डिसीज़न मॉडल चलाने वाले लोग 200ms से कम के जवाब बताते हैं — यही generative JSON से करके देखिए।
- Structural correctness. मॉडल अनुमत सेट के बाहर का आउटपुट बना ही नहीं सकता। न खराब JSON, न 'The answer is probably B because...' जैसी बकवास, और फ़ॉर्मेट गलतियाँ पकड़ने के लिए किसी guardrail layer की ज़रूरत नहीं।
- Throughput. हर रिक्वेस्ट एक ही आकार का एक पास होती है, इसलिए batching आसान और अनुमानित रहती है। परिवर्तनशील लंबाई के जवाब आपकी batching क्षमता बिगाड़ देते हैं।
- हर विकल्प के लिए एक स्कोर। आपको सिर्फ़ विजेता नहीं, पूरा डिस्ट्रीब्यूशन मिलता है। इससे abstention logic का रास्ता खुलता है: अगर शीर्ष संभावना किसी threshold से नीचे है, तो मामला किसी इंसान या बड़े मॉडल को भेज दें।
Abstention वाली बात पर ज़ोर देना ज़रूरी है। जनरेटिव जवाब एक ऐसी इकाई है जिस पर या तो भरोसा करते हैं या नहीं। विकल्पों पर संभावना-डिस्ट्रीब्यूशन आपको routing बनाने देता है: उच्च-भरोसे वाले मामले अपने-आप निकल जाते हैं, कम-भरोसे वाले आगे बढ़ाए जाते हैं। कई production triage सिस्टम के पीछे यही पैटर्न है — support ticket routing, content moderation का pre-screening, intent detection — और यहीं यह तकनीक अपनी कीमत वसूलती है। अगर आपका काम स्वाभाविक रूप से "K लेबलों में से एक चुनो, और बताओ कितने यकीन से" में बँट जाता है, तो कॉन्स्ट्रेन्ड डिकोडिंग लगभग निश्चित रूप से सही औज़ार है।
जहाँ Generative आउटपुट जीतता है
अब दूसरी तरफ़। जैसे ही आपका काम किसी fixed label set में फिट नहीं होता, कॉन्स्ट्रेन्ड डिकोडिंग टूटने लगती है। अगर जवाब कोई free-form entity, संख्या, कोड का टुकड़ा, या कुछ भी compositional है, तो आपको जनरेशन चाहिए — शायद structured-output constraints के साथ, फिर भी जनरेशन ही। एक और सूक्ष्म नुकसान है: reasoning। जब मॉडल जवाब देने से पहले chain of thought बनाता है, तो कठिन सवालों पर अक्सर उसका प्रदर्शन काफ़ी बेहतर होता है। सिंगल-पास डिसीज़न हेड आपको कोई scratchpad नहीं देता। आप system one सोच माँग रहे हैं — तेज़ और सहज — और आपको ठीक वही मिल रही है, उसकी खास विफलताओं समेत।
फिर शब्द-विन्यास की संवेदनशीलता की समस्या है। "A/B/C/D/E" पर कॉन्स्ट्रेन्ड क्लासिफायर असल में उस पोज़ीशन में हर विकल्प के टेक्स्ट के लिए मॉडल की पसंद माप रहा होता है। विकल्प C को थोड़ा दोबारा लिखिए, सूची का क्रम बदलिए, या "Answer:" को "The best answer is" कर दीजिए, और स्कोर खिसक सकते हैं। Reasoning वाले generative जवाब सतही बदलावों के प्रति ज़्यादा मज़बूत होते हैं, क्योंकि मॉडल को सिर्फ़ टोकन नहीं, कंटेंट पर टिकना पड़ता है। अगर आप दोनों में से किसी तरीके का मूल्यांकन कर रहे हैं, तो प्रस्तुति को हिलाकर देखिए कि क्या टूटता है — यह एक सस्ता robustness टेस्ट है, और असहज करने वाला भी।

कैलिब्रेशन समस्या: आपके confidence स्कोर झूठ बोल रहे हैं
यह वह जाल है जिसमें हर वह व्यक्ति फँसता है जो ऐसा कुछ बनाता है। आपको softmax से probabilities मिलती हैं, तो वे probabilities ही होंगी, है न? नहीं हैं। वे इस बात का मॉडल का confidence हैं कि कोई दिया गया टोकन आगे आएगा, और यह भाषा के बारे में कथन है, सही होने के बारे में नहीं। मॉडल से पूछिए "आपको चमगादड़ सबसे ज़्यादा कहाँ मिलेगा?" और विकल्पों में "Cave" और "Baseball game" रखिए, तो वह "Cave" को लगभग 0.998 दे देगा — एक अस्पष्ट सवाल, जिसका कोई ठोस निश्चित जवाब नहीं, उस पर लगभग पूरी निश्चितता के साथ जवाब।
असली eval पर confidence के हिसाब से बाँटें तो तस्वीर और खराब दिखती है। एक CommonsenseQA रन में, 0.9–1.0 bucket केवल लगभग 70% बार सही था, और 0.8–0.9 bucket मुश्किल से 40% तक पहुँचा। एक अच्छे कैलिब्रेटेड मॉडल को जब वह 0.9 कहे तो लगभग 90% बार सही होना चाहिए। यह मॉडल व्यवस्थित रूप से overconfident है — जो, सोचिए तो, इस पुराने अवलोकन से मेल खाता है कि आधुनिक deep nets आम तौर पर overconfident होते हैं। Guo et al. ने 2017 में दिखाया कि एक साधारण ResNet के softmax आउटपुट 1990 के उथले nets की तुलना में बुरी तरह miscalibrated हैं। पुराना सब कुछ फिर नया हो गया; हमने बस 1.7 अरब पैरामीटर पर यह समस्या दोबारा खोज ली है।
राहत की बात यह है कि इसका हल भी पुराना है और लगभग शर्मिंदा करने की हद तक सरल: temperature scaling। softmax से पहले logits को एक सीखे गए एकल scalar T से भाग दें:
def scaled_probs(logits, option_ids, temperature):
selected = logits[option_ids].float() / temperature
return torch.softmax(selected, dim=-1)
# Fit T on a validation set by minimizing negative log-likelihood
# of the correct option. For the CommonsenseQA run above, T ~= 3.8
temperature = 3.7973
probs = scaled_probs(logits, option_token_ids, temperature)
T 1 से बड़ा हो तो डिस्ट्रीब्यूशन सपाट होता है; 1 से छोटा हो तो तीखा। एक held-out सेट पर इस एक संख्या को फ़िट करने से वह भयानक calibration तालिका ईमानदार बन गई: 0.9–1.0 bucket अब लगभग 95% accuracy पर है, 0.5–0.6 bucket लगभग 55% पर। Accuracy बिल्कुल नहीं बदलती — argmax monotonic scaling के प्रति invariant है — लेकिन अब स्कोर का वही मतलब है जो आप सोचते थे। अगर आप उन probabilities के आधार पर routing करने वाले हैं, तो पहले calibrate कीजिए, वरना उन्हें इकट्ठा करने की ज़हमत न उठाइए।
क्या benchmark पर temperature tune करना धोखाधड़ी है?
चर्चा में एक जायज़ सवाल उठा: क्या T को इस तरह फ़िट करना कि मॉडल benchmark पर calibrated दिखे, थोड़ा p-hacking जैसा नहीं है? होता, अगर आप उसे test set पर फ़िट करते। ठीक से किया जाए — T को validation split पर फ़िट करें, और calibration की रिपोर्ट held-out test split पर दें — तो यह बस एक-पैरामीटर regression है, और ठीक इसी वजह से साहित्य में यह मानक तरीका है। जो अनुशासन मायने रखता है वह ML में हर जगह वाला ही है: अपने splits ईमानदार रखें, और किसी भी calibration दावे पर शक करें जो उस डेटा पर मापा गया हो जिसने फ़िटिंग को छुआ हो। अगर आपका deployment domain आपके eval domain से हटता है, तो आपका T भी हटेगा, इसलिए recalibration को बाकी सब कुछ के साथ आपके maintenance loop में रखना चाहिए।
यह असल में एक बड़ी बीमारी का उदाहरण है: उस अंतर का जो सिस्टम के लिए specified अर्थ और वह असल में जो compute करता है, उनके बीच है — यह अंतर मैं पहले भी तर्क दे चुका हूँ कि बग्स यहीं रहते हैं। "Confidence" को सही होने की संभावना के रूप में specified किया गया है; implementation आपको अगले-टोकन की plausibility देती है। Temperature scaling इस अंतर पर लगाई गई पट्टी है, इसका समाधान नहीं। जब भी softmax आउटपुट को किसी alerting threshold में जोड़ने का मन करे, यह फ़र्क याद रखिए।
एक व्यावहारिक निर्णय प्रक्रिया
अब जब मैं दो तरीकों के बीच चुनता हूँ, तो एक छोटी जाँच-सूची चलाता हूँ:
- क्या आउटपुट लेबलों का एक fixed सेट है? अगर हाँ, तो कॉन्स्ट्रेन्ड डिकोडिंग विकल्प है। अगर नहीं, तो जनरेट कीजिए।
- क्या स्वीकार्य accuracy के लिए मुझे reasoning चाहिए? दोनों का प्रोटोटाइप बनाइए। अगर सिंगल-पास हेड chain-of-thought से इतना पीछे है जितना आप सह नहीं सकते, तो latency के बावजूद जनरेशन जीतेगा।
- क्या मुझे routing या abstention के लिए हर विकल्प पर स्कोर चाहिए? अगर हाँ, तो कॉन्स्ट्रेन्ड डिकोडिंग के साथ temperature scaling लगभग मुफ़्त है, और जनरेशन इसके बराबर कुछ नहीं देता।
- लेबल सेट कितना बड़ा है? पाँच विकल्प तुच्छ हैं; पाँच हज़ार retrieval का इलाका है। बड़े label spaces में, अपने लेबल embed कीजिए, vector search से shortlist बनाइए, और फिर ही मॉडल से फ़ाइनलिस्टों में चुनाव कराइए — यह classifier-scaling की वह तरकीब है जो recommendation-system के ज़माने से मानक रही है।
- क्या लेबल बार-बार बदलेंगे? zero-shot गुण ही तो पूरा मकसद है। अगर लेबल हफ़्ते-दर-हफ़्ते बदलते हैं, तो रीट्रेन किया गया क्लासिफायर एक बोझ है; प्रॉम्प्ट में बदलाव बोझ नहीं है।
मॉडल का softmax इस बारे में दावा है कि आगे कौन-सा टोकन आएगा, इस बारे में नहीं कि क्या सच है। इसे calibrate कीजिए, वरना इस पर भरोसा मत कीजिए।
एक और बात: मॉडल का आकार इस चुनाव से जुड़ा है। छोटे मॉडल का सिंगल-पास हेड हर रिक्वेस्ट पर चलाने के लिए काफ़ी सस्ता है, यहाँ तक कि client-side भी; लोग पहले से ही ऐसे डिसीज़न मॉडल ब्राउज़र में चलाकर 200ms से कम के जवाब दे रहे हैं। एक cascaded डिज़ाइन — आसान 80% के लिए छोटा कॉन्स्ट्रेन्ड मॉडल, कठिन हिस्से के लिए बड़ा generative मॉडल — अक्सर लागत और accuracy दोनों में किसी भी अति से बेहतर निकलता है। यह वही प्रवृत्ति है जो speculative decoding के पीछे है, पर टोकन-स्तर के बजाय सिस्टम-स्तर पर लागू।
मेरी सिफ़ारिश
मेरी राय: अगर आपका काम वास्तव में fixed-choice निर्णय है — routing, triage, intent, multiple-choice evaluation — तो कॉन्स्ट्रेन्ड decision head बनाइए और पीछे मुड़कर मत देखिए। अकेली latency की जीत ही इसे सही ठहराती है, structural गारंटी parsing की पूरी श्रेणी की समस्याएँ खत्म कर देती है, और हर विकल्प के स्कोर आपको वह routing logic देते हैं जिसे जनरेशन मेल नहीं खा सकता। लेकिन कच्चे softmax को एक अनकैलिब्रेटेड उपकरण मानिए। अगर थोड़ा भी मामूली डेटासेट जुटा सकें तो अपने असली काम पर फ़ाइन-ट्यून कीजिए, एक साफ़ validation split पर temperature फ़िट कीजिए, और calibration को उस डेटा पर जाँचिए जिसे फ़िटिंग ने कभी देखा ही नहीं।
Generative आउटपुट को — जहाँ ज़रूरी हो वहाँ structured-output constraints के साथ — उन कामों के लिए बचाकर रखिए जो सचमुच compositional हैं या जिन्हें दिखने वाले reasoning से फ़ायदा होता है। और कोई भी आपको "डिसीज़न मॉडल" को नई श्रेणी बताकर न बेचने पाए: यह LLM के कोट में पहना हुआ क्लासिफायर है, जो साठ साल की discriminative modeling की संतान है, और तब सबसे उपयोगी होता है जब आप उस वंश का इतना सम्मान करें कि calibration का वह काम करें जिस पर पुराने लोग हमेशा ज़ोर देते थे। औज़ार नए हैं। अनुशासन नहीं।


