Prefill विरुद्ध decode: पहिला token उशिरा का येतो?
Prefill compute-bound असल्याने time to first token ठरवतो, तर decode memory bandwidth-bound असल्याने tokens per second ठरवतो. तुमच्या मशीनवर दोन्ही वेगळे मोजा.
एका परिच्छेदात prefill विरुद्ध decode
Prefill विरुद्ध decode हा self-hosted LLM (large language model) च्या latency विषयीच्या बहुतेक प्रश्नांचे स्पष्टीकरण देणारा मुख्य फरक आहे. Prefill संपूर्ण prompt एका pass मध्ये वाचतो आणि त्याची मर्यादा compute असते. Decode उत्तर एका वेळी एक token लिहितो आणि त्याची मर्यादा memory bandwidth असते. Time to first token हे prefill चे मोजमाप आहे. Tokens per second हे decode चे मोजमाप आहे.
दोन्ही phases एकाच GPU (graphics processing unit) वर, समान weights वापरून आणि समान process मध्ये चालतात. त्यामुळे त्यांना एकाच workload प्रमाणे पाहणे स्वाभाविक आहे. प्रत्यक्षात ते एकच device सामायिक करणाऱ्या दोन वेगवेगळ्या programs प्रमाणे वागतात. त्यांना स्वतंत्रपणे पाहिल्यावर गोंधळात टाकणाऱ्या परिणामांची मोठी यादी समजण्यास सोपी होते.
Prefill compute-bound का आहे?
Prefill मध्ये संपूर्ण prompt प्रत्येक layer मधून एकदा पाठवला जातो. 2,000 token च्या prompt मुळे प्रत्येक matrix multiply साठी 2,000 rows चे काम निर्माण होते. त्यामुळे GPU ने लोड केलेल्या weight च्या प्रत्येक byte साठी मोठ्या प्रमाणात arithmetic करावे लागते. हलवलेल्या प्रत्येक byte मागील arithmetic चे हे प्रमाण arithmetic intensity म्हणतात. Prefill मध्ये ते जास्त असते. Device जवळपास compute limit वर चालते आणि memory bus वर काही क्षमता शिल्लक राहते.
Prefill दोन गोष्टी तयार करते: प्रत्येक prompt token साठी KV cache (key आणि value tensors) आणि पहिला output token. हा pass पूर्ण होईपर्यंत reader पर्यंत काहीही पोहोचत नाही. त्यामुळे prefill time आणि time to first token (TTFT) ही मोजमापे जवळपास समान असतात.
Prompt ची लांबी वाढली की prefill cost वाढतो. Linear भाग म्हणजे प्रत्येक layer मधील matrix work. Quadratic भाग attention मुळे येतो. त्यात प्रत्येक token आधीच्या प्रत्येक token कडे लक्ष देतो. मोठ्या context वर हा भाग महत्त्वाचा ठरू लागतो. त्यामुळे prompt दुप्पट केल्यास TTFT किमान दुप्पट होते.
हे तुम्ही एका मिनिटात पाहू शकता. तुमच्या server कडे प्रथम 200 token चा prompt पाठवा. त्यानंतर 2,000 token चा prompt पाठवा. दोन्ही वेळा समान संख्येतील output tokens मागा. TTFT झपाट्याने वाढतो. पहिल्या token नंतरचा streaming speed मात्र फारसा बदलत नाही.
डिकोड memory bandwidth-बद्ध का का असतो?
डिकोड प्रत्येक step मध्ये एक token तयार करते. तो एकच token तयार करण्यासाठी GPU ने model मधील प्रत्येक weight memory मधून वाचला पाहिजे, प्रत्येक weight काही operations साठी वापरला पाहिजे आणि नंतर तो टाकून दिला पाहिजे. Arithmetic intensity जवळपास 1 असते. त्यामुळे compute units आपला बहुतांश वेळ प्रतीक्षा करण्यात घालवतात.
प्रत्येक token साठी संपूर्ण model memory मधून वाचावा लागतो. त्यामुळे decode मंद असतो. Memory bus गती ठरवतो आणि compute units idle राहतात.
यामुळे single-stream decode speed ची कमाल मर्यादा कागदावर arithmetic करून काढता येते. Memory bandwidth ला weights व्यापत असलेल्या bytes ने भागा.
The data behind this chart
[
{
"device": "CPU, dual channel DDR5-5600",
"mem_bandwidth_gb_s": 90,
"decode_ceiling_tok_s": 6
},
{
"device": "NVIDIA A10G",
"mem_bandwidth_gb_s": 600,
"decode_ceiling_tok_s": 38
},
{
"device": "NVIDIA L40S",
"mem_bandwidth_gb_s": 864,
"decode_ceiling_tok_s": 54
},
{
"device": "NVIDIA RTX 4090",
"mem_bandwidth_gb_s": 1008,
"decode_ceiling_tok_s": 63
},
{
"device": "NVIDIA A100 80GB SXM",
"mem_bandwidth_gb_s": 2039,
"decode_ceiling_tok_s": 127
},
{
"device": "NVIDIA H100 SXM",
"mem_bandwidth_gb_s": 3350,
"decode_ceiling_tok_s": 209
}
]Bandwidth स्तंभात प्रत्येक vendor ने प्रकाशित केलेला specification figure आहे. Ceiling स्तंभात त्या आकड्याला 16 GB ने भागले आहे. 16 bit precision मध्ये साठवलेल्या 8 billion parameter model चा हा आकार आहे. ही गणना आहे; benchmark result नाही. तुमचा मोजलेला rate यापेक्षा कमी असेल. तो किती कमी आहे हे जाणून घेणे उपयुक्त आहे, कारण त्यावरून serving stack दुरुस्त करायचा की hardware हे ठरवता येते.
6 rows क्रमाने वाचा. Pattern स्पष्ट दिसतो. Dual-channel DDR5 वरील CPU सुमारे 90 GB/s data हलवतो. त्यामुळे त्या model साठी decode जवळपास 6 tokens per second इतका मर्यादित राहतो. L40S जवळपास 54 पर्यंत पोहोचतो. Published bandwidth 3350 GB/s असलेला H100 SXM जवळपास 209 वर राहतो.
Quantization हा decode speed वाढवण्यासाठी सर्वात प्रभावी एकल उपाय का आहे, हेही यावरून स्पष्ट होते. तोच model 16 bits ऐवजी 8 bits मध्ये साठवल्यास प्रत्येक token साठी वाचावे लागणारे bytes निम्मे होतात. त्यामुळे ceiling साधारणपणे दुप्पट होते. Compute वाढत नाही. कमी memory हलवावी लागते.
मी माझ्या सर्व्हरवरील प्रत्येक टप्प्याचे मापन कसे करू?
Ollama प्रतिसादाच्या body मध्ये हा विभाग देतो. streaming नसलेला completion मागवा आणि counters वाचा.
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Explain memory bandwidth in two sentences.",
"stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'तुम्ही प्रत्यक्षात pull केलेला model tag वापरा. तो tag ollama list दाखवेल. prompt_eval_count आणि prompt_eval_duration हे prefill शी संबंधित आहेत: prompt token count आणि त्यावर लागलेला वेळ. eval_count आणि eval_duration हे decode शी संबंधित आहेत. कालावधी nanoseconds मध्ये असतात. त्यामुळे decode speed eval_count / eval_duration * 1e9 आणि prefill speed prompt_eval_count / prompt_eval_duration * 1e9 अशी मोजली जाते. त्याच request साठी prefill rate decode rate पेक्षा खूप जास्त असणे अपेक्षित आहे. इतर सर्व स्पष्टीकरणे या फरकाचे कारण सांगतात.
vLLM सारख्या OpenAI compatible server साठी curl पहिला byte येईपर्यंतचा वेळ मोजू शकतो.
curl -N -s -o /dev/null \
-w 'pretransfer %{time_pretransfer}s first_byte %{time_starttransfer}s\n' \
http://localhost:8000/v1/completions \
-H 'Content-Type: application/json' \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'time_starttransfer म्हणजे response body मधील पहिला byte येण्याचा क्षण. त्यामुळे "stream": true सह हे TTFT आणि connection setup यांचा एकत्रित वेळ आहे. setup cost वगळण्यासाठी time_pretransfer वजा करा. ही चाचणी दोनदा चालवा आणि दुसरा निकाल ठेवा. पहिल्या call मध्ये model cold load होण्याचा वेळ समाविष्ट असू शकतो.
vLLM /metrics वर Prometheus metrics देखील प्रकाशित करतो. curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' चालवल्यावर vllm:time_to_first_token_seconds आणि vllm:inter_token_latency_seconds हे histograms मिळतात. queue depth साठी vllm:num_requests_running आणि vllm:num_requests_waiting, तसेच cache pressure साठी vllm:kv_cache_usage_perc जोडा. हे पाच names मिळून संपूर्ण dashboard तयार होते.
लोड असताना vllm bench serve --model <name> --num-prompts 200 --request-rate 4 चालू server वर काम करते आणि time to first token तसेच प्रत्येक output token साठीची latency percentiles सह नोंदवते. दोन टप्प्यांमध्ये परस्पर संघर्ष कसा होतो हे पाहण्याचा हा एकमेव मार्ग आहे. कोणतेही tuning करण्यापूर्वी स्वच्छ baseline घ्या. स्थानिक LLM वर प्रति सेकंद tokens मोजण्याची पद्धत वापरल्यास reboot नंतरही टिकणारा baseline मिळतो.
दीर्घ system prompt मुळे पहिल्या token ला विलंब का होतो, पण streaming speed वर परिणाम का होत नाही?
कारण system prompt हे prefill काम असते, एवढेच. पहिला token दिसण्यापूर्वी ते उर्वरित prompt प्रमाणेच, त्याच pass मध्ये, एकदा process केले जाते. त्या pass नंतर ते केवळ KV cache entries म्हणून उपलब्ध राहते आणि decode प्रक्रियेत इतर सर्व entries सोबत वाचले जाते. त्यामुळे 3,000 token च्या system prompt मुळे प्रत्येक request साठी TTFT वाढतो, पण tokens per second जवळजवळ बदलत नाही.
जवळजवळ, पण पूर्णपणे नाही. त्या अतिरिक्त KV entries प्रत्येक decode step मध्ये पुन्हा वाचल्या जातात. त्यामुळे खूप मोठा prompt decode प्रक्रिया थोडी मंद करतो. पुढील section मध्ये याचे स्पष्टीकरण आहे.
यावरील उपाय म्हणजे तोच prefix पुन्हा पुन्हा recompute करणे थांबवणे. prefix caching असलेला server shared prefix चा KV cache जतन करून त्याचा पुनर्वापर करतो. त्यामुळे तोच system prompt असलेला दुसरा request prefill मधील तो भाग पूर्णपणे वगळतो. vLLM याला automatic prefix caching म्हणते. तुमच्या version मध्ये vllm serve --help तपासा, कारण releases नुसार default बदलले आहे. GPU मधील हा KV cache, API provider कडून billing साठी वापरल्या जाणाऱ्या prompt cache पेक्षा वेगळा आहे. त्यामुळे कोणत्याही cache चे tuning करण्यापूर्वी KV cache आणि prompt cache मधील फरक वाचणे उपयुक्त ठरेल.
कॉन्टेक्स्ट भरत असताना decode धीमे का होते?
याची दोन कारणे आहेत. दोन्ही कारणे KV cache शी संबंधित आहेत.
पहिले कारण bandwidth आहे. प्रत्येक decode step मध्ये attention मागील प्रत्येक token चे keys आणि values वाचते. प्रत्येक token साठी weights ची किंमत स्थिर असते. KV cache मात्र वाढत जाते. मॉडेलच्या config.json वरून तिचा आकार काढता येतो: प्रति token bytes = 2 गुणिले num_hidden_layers, गुणिले num_key_value_heads, गुणिले head dimension (hidden_size भागिले num_attention_heads), गुणिले प्रति element bytes. सुरुवातीचा 2 हा एक key आणि एक value मोजतो.
सामान्य 8 billion parameter layout मध्ये, 32 layers, GQA (grouped query attention) अंतर्गत 8 key आणि value heads, head dimension 128 आणि 16 bit precision असल्यास, गणना 2 x 32 x 8 x 128 x 2 = 131,072 bytes अशी होते. म्हणजे प्रति token सुमारे 128 KiB. त्यामुळे 8,000 token च्या संभाषणासाठी प्रत्येक request ला सुमारे 1 GB KV cache लागते.
दुसरे कारण capacity आहे. तो 1 GB memory weights किंवा अन्य user चा context ठेवण्यासाठी वापरता येत नाही. Server startup वेळी KV pool चा आकार ठरवतो. vLLM मध्ये हे --gpu-memory-utilization द्वारे नियंत्रित केले जाते. Pool पूर्ण झाल्यावर नवीन requests प्रतीक्षा करतात. vllm:num_requests_waiting वाढत असताना vllm:kv_cache_usage_perc जवळपास 1 वर राहणे ही या स्थितीची अचूक खूण आहे. काही stacks request queue मध्ये ठेवण्याऐवजी सुरू असलेल्या request ला preempt करतात आणि नंतर तिचा cache पुन्हा तयार करतात. वापरकर्त्याला याचा अनुभव stream च्या मध्यात stall म्हणून येतो.
Long context ची किंमत दोन प्रकारे मोजावी लागते: सुरुवातीला अधिक prefill काम आणि उत्तराच्या उर्वरित भागासाठी प्रत्येक token वाचताना अधिक memory read.
बॅचिंगमुळे throughput का वाढतो आणि tail latency का खराब होते?
Decode हे bandwidth-bound असल्यामुळे compute बाजूने अतिरिक्त requests ची किंमत जवळपास शून्य असते. Weights चे एक read batch मधील प्रत्येक sequence साठी एक token तयार करू शकते. त्यामुळे batch size वाढवत नेल्यास एकूण throughput जवळपास रेषीय पद्धतीने वाढतो, जोपर्यंत KV pool संपत नाही किंवा batch इतका मोठा होत नाही की तो पुन्हा compute-bound होतो. Continuous batching प्रत्येक step ला batch नव्याने तयार करते. त्यामुळे पूर्ण झालेली request बाहेर पडते आणि queue मधील request तिच्या शेजारच्या requests ची वाट न पाहता सामील होते.
याची किंमत percentiles मध्ये दिसते. प्रत्येक user's next token आता shared step मधील सर्वांत धीम्या भागाची वाट पाहतो. त्यामुळे p50, म्हणजे median, स्वीकारार्ह राहतो; पण p99, म्हणजे 100 पैकी सर्वांत धीम्या 1 request चा latency, वाढतो. लोकांना p99 जाणवतो, कारण वाक्याच्या मध्यभागी जाणवणारा विराम तोच असतो.
Prefill मुळे हा परिणाम आणखी तीव्र होतो. Stream सुरू असताना मोठा prompt आल्यास device एक दीर्घ step व्यापला जातो आणि त्या वेळी streaming करणाऱ्या प्रत्येकाला gap दिसतो. Chunked prefill मोठ्या prompt चे तुकडे करून आणि प्रत्येक तुकडा decode batches मध्ये मिसळून ही समस्या मोठ्या प्रमाणात कमी करते. August 2026 पर्यंत vLLM V1 engine हे default ने करते आणि हा समतोल --max-num-batched-tokens द्वारे नियंत्रित करता येतो. vLLM tuning documentation मध्ये हा tradeoff स्पष्टपणे दिला आहे: सुमारे 2048 सारखी लहान values चांगली inter token latency (ITL) देतात, कारण कमी prefills decode मध्ये व्यत्यय आणतात; मोठ्या values चांगले TTFT देतात, कारण एका batch मध्ये अधिक prefill tokens बसतात. हा एकच flag prefill आणि decode यांतील समतोल number म्हणून उपलब्ध करून देतो. p99 कोणत्या बिंदूवर स्वीकारार्ह राहत नाही हा capacity चा प्रश्न आहे. एक self-hosted LLM किती concurrent users ना सेवा देऊ शकतो हे त्याच metrics च्या आधारे ठरवता येते.
मोठा GPU कधी कधी काहीही बदल का घडवून आणत नाही?
कारण मोठा GPU म्हणजे बहुतेक वेळा अधिक compute. पण decode साठी compute ची गरज नसते.
वरील chart मधील दोन rows ची तुलना करा. A100 80GB ची published bandwidth 2039 GB/s आहे, तर L40S ची 864 GB/s आहे. Decode ceiling देखील त्याच प्रमाणात आहे: अनुक्रमे 127 tokens per second आणि 54. बहुतेक मोजमापांनुसार RTX 4090 हे अतिशय वेगवान card आहे. त्याची 1008 GB/s bandwidth त्याची ceiling 63 पर्यंत नेते. दोन cards मध्ये इतर कोणतेही फरक असले, तरी single stream decode हे specification sheet वरील bandwidth line शी सुसंगत राहते.
म्हणून decode वेगवान करण्याचे दोन मार्ग आहेत: प्रत्येक token साठी कमी bytes वाचणे (weights quantize करणे किंवा लहान model चालवणे) किंवा अधिक bandwidth खरेदी करणे. Prefill हे याच्या उलट आहे. त्याला compute आवश्यक असते. त्यामुळे वेगवान card मुळे लांब prompts साठी TTFT खरोखर कमी होते. तक्रार अशी असेल की पहिला token येण्यासाठी चार seconds लागतात, तर चांगले hardware समस्या सोडवू शकते. तक्रार अशी असेल की text हळूहळू type होते, तर बहुधा तसे होणार नाही.
प्रिफिल आणि डीकोड वेगवेगळ्या workers वर चालवावे का?
मोठ्या serving stacks मध्ये हेच केले जाते. या तंत्राला prefill and decode disaggregation म्हणतात. Workers चा एक pool केवळ prefill चालवतो. दुसरा pool केवळ decode चालवतो. पहिल्या pool ने तयार केलेला KV cache fast interconnect द्वारे दुसऱ्या pool कडे पाठवला जातो. हे कार्य करते कारण या दोन्ही टप्प्यांना वेगवेगळे hardware आणि वेगवेगळे scheduling आवश्यक असते. Prefill ला compute आणि मोठे token batches आवश्यक असतात. Decode ला bandwidth आणि अनेक concurrent sequences आवश्यक असतात. त्यांना वेगळे केल्यामुळे प्रत्येक pool स्वतंत्रपणे scale करता येतो. तसेच एक मोठा prompt सर्व active streams ला थांबवून ठेवत नाही.
एकाच GPU असलेल्या single VPS (virtual private server) वर हे करणे जवळजवळ कधीही फायदेशीर ठरत नाही. तुम्ही एकाच device चे संसाधन त्याच्याविरुद्ध विभागत असता. तसेच cache मधील gigabytes चे pointer network transfer मध्ये रूपांतरित करता. प्रत्येक phase साठी स्वतंत्र संपूर्ण machines नियुक्त करता येतील इतके accelerators आणि दोन्ही pools व्यस्त ठेवण्यासाठी पुरेसा स्थिर traffic उपलब्ध झाल्यावर हे तंत्र फायदेशीर ठरते. त्यापेक्षा कमी स्तरावर, एका flag मुळे chunked prefill जवळपास तेवढेच isolation देते.
संख्या योग्य नसल्यास काय बदलावे
TTFT खूप जास्त असल्यास:
- Prompt लहान करा. Prefill चा खर्च prompt tokens च्या संख्येनुसार वाढतो आणि प्रत्येक request वर system prompt साठी पुन्हा खर्च होतो.
- Prefix caching सुरू करा. त्यामुळे वारंवार येणारा prefix प्रत्येक वेळी पुन्हा compute करण्याऐवजी एकदाच compute केला जातो.
--max-num-batched-tokensवाढवा, जेणेकरून प्रत्येक step मध्ये अधिक prefill work होईल.- Model ला दोष देण्यापूर्वी queue तपासा.
vllm:num_requests_waitingशून्यापेक्षा जास्त असल्यास request सुरू झालेली नव्हती. ही capacity ची समस्या आहे.
Tokens per second खूप कमी असल्यास:
- Weights quantize करा. प्रत्येक weight साठी कमी bytes असल्यास प्रत्येक token साठी कमी bytes read करावे लागतात.
- तुमच्या card ची प्रकाशित memory bandwidth वरील chart शी तपासा आणि तुम्ही कमाल मर्यादेच्या किती जवळ आहात ते पाहा.
--max-num-batched-tokensकमी करा, जेणेकरून prefill मुळे decode मध्ये कमी वेळा व्यत्यय येईल.- Context length तपासा. हजारो tokens पर्यंत वाढलेल्या conversation मध्ये प्रत्येक step ला खूप मोठा KV cache read केला जातो.
येथे runtime देखील महत्त्वाचे आहे, कारण Ollama आणि vLLM prefill आणि decode वेगवेगळ्या पद्धतीने schedule करतात, आणि एका runtime साठी उपयुक्त ठरणारी setting दुसऱ्यावर काहीही परिणाम करू शकत नाही. प्रथम दोन्ही phases मध्ये measurement करा. त्यानंतर एकावेळी एकच बदल करा.
FAQ
माझ्या पहिल्या token ला काही सेकंद का लागतात, पण त्यानंतरचे token जलद का stream होतात?
हा विलंब prefill मुळे होतो आणि streaming decode मुळे होते. कोणतेही output तयार होण्यापूर्वी prefill संपूर्ण prompt वर एकाच compute-bound pass मध्ये प्रक्रिया करते. त्यामुळे prompt ची लांबी वाढल्यास त्याचा खर्च वाढतो. त्यानंतर decode प्रत्येक step मध्ये एक token तयार करते. हा वेग memory bandwidth ठरवते आणि prompt किती लांब आहे यावर त्याचा फारसा परिणाम होत नाही. प्रत्येक request मध्ये मोठा system prompt पाठवणे हे याचे सामान्य कारण आहे. Prefix caching मुळे हा पुनरावृत्ती होणारा खर्च कमी होतो.
मोठा prompt असल्यास tokens per second चा वेग कमी होतो का?
थोडासा कमी होतो, पण याचे कारण TTFT पेक्षा वेगळे आहे. प्रत्येक decode step मध्ये आधीच्या सर्व tokens चे keys आणि values वाचावे लागतात. त्यामुळे मोठ्या KV cache मुळे प्रत्येक token साठी अधिक bytes वाचावे लागतात. सामान्य 8 billion parameter layout मध्ये cache प्रत्येक token साठी सुमारे 128 KiB असतो. त्यामुळे 8,000 token context मध्ये प्रत्येक step वेळी सुमारे 1 GB data वाचला जातो. तरीही मोठ्या prompt चा मुख्य परिणाम streaming speed वर नसून TTFT वर होतो.
Decode speed चा अंदाज कोणती GPU specification देते?
Memory bandwidth. Published bandwidth ला memory मधील weights च्या आकाराने भागल्यास एका stream साठीची arithmetic ceiling मिळते. अधिक compute असलेले, पण समान bandwidth असलेले card अधिक जलद stream करेलच असे नाही. 8 bits मध्ये quantizing केल्यावर decode speed साधारणपणे दुप्पट होण्याचे हेच कारण आहे: compute न बदलता प्रत्येक token साठी वाचावे लागणारे bytes निम्मे होतात.
अधिक users जोडल्यावर throughput का वाढतो, पण प्रत्येक user ला सेवा धीमी का वाटते?
Weights चे एकदा केलेले read batch मधील प्रत्येक sequence साठी एक token तयार करण्यासाठी वापरले जाते. त्यामुळे batch size वाढल्यावर एकूण tokens per second वाढतात. मात्र प्रत्येक individual token ला shared step ची प्रतीक्षा करावी लागते. त्यामुळे त्याच वेळी प्रत्येक user साठी latency वाढते. Aggregate throughput number ऐवजी p99 inter token latency monitor करा. तसेच requests चालू आहेत की queue मध्ये प्रतीक्षा करत आहेत हे पाहण्यासाठी vllm:num_requests_waiting तपासा.