Prefill বনাম Decode: LLM ল্যাটেন্সি কেন আলাদা হয়?
Prefill হলো কম্পিউট-নির্ভর যা Time to First Token নির্ধারণ করে এবং Decode হলো মেমোরি ব্যান্ডউইথ-নির্ভর যা Tokens per Second নিয়ন্ত্রণ করে। এই দুটি পর্যায় কেন আলাদা তা জানুন।
Prefill বনাম decode, এক অনুচ্ছেদে
Prefill বনাম decode-এর পার্থক্যটি self-hosted LLM (large language model)-এর ল্যাটেন্সি সংক্রান্ত অধিকাংশ প্রশ্নের উত্তর দেয়। Prefill পুরো প্রম্পটটি একবারে পড়ে এবং এটি কম্পিউট ক্ষমতার ওপর নির্ভরশীল। Decode উত্তরটি একটি করে টোকেন হিসেবে লেখে এবং এটি মেমোরি ব্যান্ডউইথের ওপর নির্ভরশীল। Time to first token হলো একটি prefill সংখ্যা। Tokens per second হলো একটি decode সংখ্যা।
উভয় পর্যায় একই GPU (graphics processing unit)-তে, একই weights ব্যবহার করে এবং একই প্রসেসের ভেতরে চলে, তাই এদের একটি ওয়ার্কলোড হিসেবে বিবেচনা করা স্বাভাবিক। তবে এরা একই ডিভাইস শেয়ার করা দুটি ভিন্ন প্রোগ্রামের মতো আচরণ করে। এদের আলাদা করলে বিভ্রান্তিকর ফলাফলের দীর্ঘ তালিকাটি আর বিভ্রান্তিকর মনে হবে না।
কেন prefill compute bound বা কম্পিউট-নির্ভর?
Prefill প্রতিটি লেয়ারের মধ্য দিয়ে পুরো প্রম্পটটিকে একবার চালনা করে। 2,000 টোকেনের একটি প্রম্পট প্রতিটি ম্যাট্রিক্স মাল্টিপ্লিকেশনকে 2,000 সারির কাজ দেয়, তাই GPU প্রতিটি বাইট ওয়েট লোড করার বিপরীতে প্রচুর গাণিতিক হিসাব সম্পন্ন করে। এই অনুপাতটিকে, অর্থাৎ প্রতি বাইট ডেটা স্থানান্তরের বিপরীতে গাণিতিক কাজের পরিমাণকে, arithmetic intensity বলা হয় এবং prefill-এর ক্ষেত্রে এটি অনেক বেশি। ডিভাইসটি তখন তার compute limit বা সর্বোচ্চ হিসাব ক্ষমতার কাছাকাছি কাজ করে এবং memory bus-এ কিছুটা অলস সময় থাকে।
Prefill দুটি জিনিস তৈরি করে: প্রতিটি প্রম্পট টোকেনের জন্য KV cache (key এবং value tensors) এবং প্রথম আউটপুট টোকেন। এই প্রক্রিয়া শেষ না হওয়া পর্যন্ত কোনো কিছুই ব্যবহারকারীর কাছে পৌঁছায় না, আর এই কারণেই prefill-এর সময় এবং time to first token (TTFT) প্রায় একই পরিমাপ নির্দেশ করে।
Prefill-এর খরচ প্রম্পটের দৈর্ঘ্যের সাথে বৃদ্ধি পায়। এর রৈখিক (linear) অংশটি হলো প্রতি লেয়ারের ম্যাট্রিক্সের কাজ। এর দ্বিঘাত (quadratic) অংশটি হলো attention, যেখানে প্রতিটি টোকেন তার আগের প্রতিটি টোকেনের সাথে সম্পর্ক স্থাপন করে, এবং দীর্ঘ context-এর ক্ষেত্রে এটি গুরুত্বপূর্ণ হয়ে ওঠে। তাই প্রম্পটের দৈর্ঘ্য দ্বিগুণ করলে TTFT অন্তত দ্বিগুণ হয়ে যায়।
আপনি এক মিনিটেই এটি পর্যবেক্ষণ করতে পারেন। আপনার সার্ভারে 200 টোকেনের একটি প্রম্পট পাঠান, তারপর 2,000 টোকেনের একটি প্রম্পট পাঠান, এবং প্রতিবার একই সংখ্যক আউটপুট টোকেন চান। দেখবেন TTFT দ্রুত বৃদ্ধি পাচ্ছে। কিন্তু প্রথম টোকেন আসার পর streaming speed-এ তেমন কোনো পরিবর্তন হয় না।
ডিকোড (decode) কেন মেমোরি ব্যান্ডউইথ দ্বারা সীমাবদ্ধ?
ডিকোড প্রক্রিয়ায় প্রতি ধাপে একটি করে টোকেন তৈরি হয়। এই একটি টোকেন তৈরি করার জন্য GPU-কে মেমোরি থেকে মডেলের প্রতিটি ওয়েট (weight) পড়তে হয়, প্রতিটি ওয়েট দিয়ে অল্প কিছু গাণিতিক অপারেশন সম্পন্ন করতে হয় এবং তারপর সেটিকে ফেলে দিতে হয়। এক্ষেত্রে অ্যারিথমেটিক ইনটেনসিটি (arithmetic intensity) 1-এর কাছাকাছি থাকে, তাই কম্পিউট ইউনিটগুলো তাদের সময়ের বেশিরভাগ অংশই অলস বসে থেকে কাটায়।
ডিকোড ধীরগতির হওয়ার কারণ হলো প্রতিটি টোকেনের জন্য মেমোরি থেকে পুরো মডেলটি পড়তে হয়, তাই মেমোরি বাসই কাজের গতি নির্ধারণ করে এবং কম্পিউট ইউনিটগুলো অলস থাকে।
এর ফলে সিঙ্গেল স্ট্রিম ডিকোড গতির সর্বোচ্চ সীমাটি কাগজে-কলমে হিসাব করা সম্ভব। মেমোরি ব্যান্ডউইথকে ওয়েটগুলো যে পরিমাণ বাইট দখল করে তা দিয়ে ভাগ করুন।
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
}
]ব্যান্ডউইথ কলামে প্রতিটি ভেন্ডরের প্রকাশিত স্পেসিফিকেশন মান দেওয়া আছে। সিলিং কলামটি হলো সেই মানকে 16 GB দিয়ে ভাগফল, যা 16-বিট প্রিসিশনে সংরক্ষিত 8 বিলিয়ন প্যারামিটারের একটি মডেলের আকার। এটি একটি গাণিতিক হিসাব, কোনো বেঞ্চমার্ক ফলাফল নয়। আপনার পরিমাপ করা হার এর নিচে থাকবে, এবং এটি কতটা নিচে তা জানা জরুরি, কারণ এটি আপনাকে বুঝতে সাহায্য করবে যে আপনার সার্ভিং স্ট্যাক ঠিক করা প্রয়োজন নাকি হার্ডওয়্যার।
6 সংখ্যক সারি ক্রমানুসারে পড়লে প্যাটার্নটি স্পষ্ট হয়ে ওঠে। ডুয়াল চ্যানেল DDR5-এর একটি CPU প্রায় 90 GB/s গতিতে ডেটা স্থানান্তর করে, যা ওই মডেলের জন্য ডিকোড গতিকে প্রায় 6 টোকেন প্রতি সেকেন্ডে সীমাবদ্ধ করে। একটি L40S-এর ক্ষেত্রে এই হার 54-এর কাছাকাছি থাকে। একটি H100 SXM, যার প্রকাশিত ব্যান্ডউইথ 3350 GB/s, সেটি 209-এর কাছাকাছি অবস্থান করে।
এ কারণেই ডিকোড গতি বাড়ানোর সবচেয়ে কার্যকর উপায় হলো কোয়ান্টাইজেশন (quantization)। একই মডেলকে 16 বিটের পরিবর্তে 8 বিটে সংরক্ষণ করলে প্রতি টোকেনের জন্য পঠিত বাইটের পরিমাণ অর্ধেক হয়ে যায়, ফলে সর্বোচ্চ গতি প্রায় দ্বিগুণ হয়ে যায়। আপনি কোনো বাড়তি কম্পিউটেশন যোগ করেননি, বরং কম মেমোরি ব্যবহার করেছেন।
আমি আমার নিজস্ব সার্ভারে প্রতিটি ধাপ কীভাবে পরিমাপ করব?
Ollama রেসপন্স বডিতে এই বিভাজনটি প্রদান করে। একটি non-streaming completion-এর অনুরোধ করুন এবং কাউন্টারগুলো পড়ুন।
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 করেছেন, যা ollama list আপনাকে দেখাবে। prompt_eval_count এবং prompt_eval_duration হলো prefill: প্রম্পট টোকেন সংখ্যা এবং এতে ব্যয় হওয়া সময়। eval_count এবং eval_duration হলো decode। সময়কাল ন্যানোসেকেন্ডে দেওয়া থাকে, তাই decode গতি হলো eval_count / eval_duration * 1e9 এবং prefill গতি হলো prompt_eval_count / prompt_eval_duration * 1e9। একই অনুরোধে prefill রেট decode রেটের চেয়ে অনেক বেশি হওয়ার প্রত্যাশা করুন। এই পার্থক্যটিই এখানে বাকি সবকিছু ব্যাখ্যা করে।
vLLM-এর মতো OpenAI-সামঞ্জস্যপূর্ণ সার্ভারের ক্ষেত্রে, curl আপনার জন্য প্রথম বাইটের সময় পরিমাপ করতে পারে।
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 হলো সেই মুহূর্ত যখন বডির প্রথম বাইটটি পৌঁছায়, তাই "stream": true-এর সাথে এটি হলো TTFT এবং কানেকশন সেটআপের যোগফল। সেটআপের খরচ বাদ দিতে time_pretransfer বিয়োগ করুন। এটি দুইবার চালান এবং দ্বিতীয় ফলাফলটি রাখুন, কারণ প্রথম কলে কোল্ড মডেল লোড অন্তর্ভুক্ত থাকতে পারে।
vLLM এছাড়াও /metrics-এ Prometheus মেট্রিক্স হিসেবে এই বিভাজন প্রকাশ করে। 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 হিস্টোগ্রামগুলো পাবেন। কিউ ডেপথের জন্য vllm:num_requests_running এবং vllm:num_requests_waiting, এবং ক্যাশ প্রেশারের জন্য vllm:kv_cache_usage_perc যোগ করুন। এই পাঁচটি নামই পুরো ড্যাশবোর্ড তৈরি করে।
লোড চলাকালীন, vllm bench serve --model <name> --num-prompts 200 --request-rate 4 চলমান সার্ভারকে পরিচালনা করে এবং পার্সেন্টাইলসহ প্রথম টোকেনের সময় ও প্রতি আউটপুট টোকেনের ল্যাটেন্সি রিপোর্ট করে, যা দুটি ধাপের মধ্যকার লড়াই দেখার একমাত্র উপায়। কোনো কিছু টিউন করার আগে, একটি পরিষ্কার বেসলাইন তৈরি করুন: লোকাল LLM-এ প্রতি সেকেন্ডে টোকেন পরিমাপ-এর পদ্ধতিটি আপনাকে এমন একটি বেসলাইন দেবে যা রিবুট করার পরেও টিকে থাকে।
কেন দীর্ঘ সিস্টেম প্রম্পট প্রথম টোকেনে দেরি করায় কিন্তু স্ট্রিমিং গতিতে প্রভাব ফেলে না?
কারণ সিস্টেম প্রম্পট হলো প্রিফিল (prefill) সংক্রান্ত কাজ, এর বাইরে আর কিছু নয়। প্রথম টোকেন আসার আগে প্রম্পটের বাকি অংশের সাথে এটি একবারই প্রসেস করা হয়। সেই ধাপের পর এটি কেবল KV cache এন্ট্রি হিসেবে থাকে এবং ডিকোড করার সময় অন্য সবকিছুর সাথে এটিও পড়া হয়। তাই 3,000 টোকেনের একটি সিস্টেম প্রম্পট প্রতিটি রিকোয়েস্টের TTFT বাড়িয়ে দেয়, কিন্তু প্রতি সেকেন্ডে টোকেন সংখ্যা প্রায় অপরিবর্তিত থাকে।
প্রায় অপরিবর্তিত, পুরোপুরি নয়। এই অতিরিক্ত KV এন্ট্রিগুলো প্রতিটি ডিকোড ধাপে পুনরায় পড়া হয়, তাই খুব দীর্ঘ প্রম্পট ডিকোড করার গতি কিছুটা কমিয়ে দেয়। পরবর্তী সেকশনে এটি নিয়ে আলোচনা করা হয়েছে।
এর সমাধান হলো একই প্রিফিক্স বারবার রিকম্পিউট করা বন্ধ করা। প্রিফিক্স ক্যাশিং (prefix caching) সুবিধা আছে এমন সার্ভার একটি শেয়ারড প্রিফিক্সের KV cache সংরক্ষণ করে এবং তা পুনরায় ব্যবহার করে। ফলে একই সিস্টেম প্রম্পট বহনকারী দ্বিতীয় রিকোয়েস্টটি প্রিফিলের সেই অংশটি পুরোপুরি এড়িয়ে যায়। vLLM একে অটোমেটিক প্রিফিক্স ক্যাশিং বলে; আপনার ভার্সনে vllm serve --help চেক করুন, কারণ বিভিন্ন রিলিজের সাথে ডিফল্ট সেটিংস পরিবর্তিত হয়েছে। GPU-এর ভেতরের সেই KV cache এবং API প্রোভাইডার যে প্রম্পট ক্যাশের জন্য বিল করে, তা এক নয়। তাই যেকোনো একটি টিউন করার আগে KV cache এবং প্রম্পট ক্যাশের পার্থক্য পড়ে নেওয়া জরুরি।
কনটেক্সট পূর্ণ হওয়ার সাথে সাথে ডিকোড কেন ধীর হয়ে যায়?
এর দুটি কারণ রয়েছে, যার উভয়ই KV cache-এর সাথে সম্পর্কিত।
প্রথম কারণটি হলো ব্যান্ডউইথ। প্রতিটি ডিকোড ধাপে, অ্যাটেনশন মেকানিজম পূর্ববর্তী প্রতিটি টোকেনের কি (key) এবং ভ্যালু (value) পড়ে। ওয়েট (weights)-এর খরচ প্রতিটি টোকেনের জন্য নির্দিষ্ট, কিন্তু KV cache সময়ের সাথে বাড়তে থাকে। আপনি মডেলের config.json থেকে এর আকার হিসাব করতে পারেন: প্রতি টোকেনে বাইট সংখ্যা হলো 2 গুণ num_hidden_layers, গুণ num_key_value_heads, গুণ হেড ডাইমেনশন (hidden_size ভাগ num_attention_heads), গুণ প্রতি এলিমেন্টে বাইট সংখ্যা। শুরুর 2 সংখ্যাটি একটি কি এবং একটি ভ্যালুকে নির্দেশ করে।
একটি সাধারণ 8 বিলিয়ন প্যারামিটার লেআউট, 32 লেয়ার, GQA (grouped query attention)-এর অধীনে 8টি কি এবং ভ্যালু হেড, 128 হেড ডাইমেনশন এবং 16 বিট প্রিসিশনের ক্ষেত্রে হিসাবটি দাঁড়ায়: 2 x 32 x 8 x 128 x 2 = 131,072 বাইট, অর্থাৎ প্রতি টোকেনে প্রায় 128 KiB। সুতরাং, 8,000 টোকেনের একটি কথোপকথনে প্রতি রিকোয়েস্টের জন্য প্রায় 1 GB KV cache প্রয়োজন হয়।
দ্বিতীয় কারণটি হলো ধারণক্ষমতা। এই 1 GB মেমোরি এমন জায়গা দখল করে যা অন্য কোনো ওয়েট বা অন্য ব্যবহারকারীর কনটেক্সট ধরে রাখতে পারে না। সার্ভার শুরুতে একবারই তার KV পুলের আকার নির্ধারণ করে, vLLM-এর ক্ষেত্রে এটি --gpu-memory-utilization-এর মাধ্যমে হয়। যখন এই পুল পূর্ণ হয়ে যায়, তখন নতুন রিকোয়েস্টগুলোকে অপেক্ষা করতে হয়। vllm:num_requests_waiting বৃদ্ধি পাওয়া এবং একই সাথে vllm:kv_cache_usage_perc-এর মান 1-এর কাছাকাছি থাকা এই অবস্থারই স্পষ্ট লক্ষণ। কিছু স্ট্যাক রানিং রিকোয়েস্টকে প্রি-এম্পট (preempt) করে এবং কিউতে রাখার পরিবর্তে পরে তার ক্যাশ পুনরায় হিসাব করে, যা ব্যবহারকারী স্ট্রিম চলাকালীন মাঝপথে আটকে যাওয়া বা স্থবিরতা হিসেবে অনুভব করেন।
দীর্ঘ কনটেক্সটের জন্য আপনাকে দ্বিগুণ মূল্য দিতে হয়: শুরুতে প্রিফিল (prefill) করার জন্য বাড়তি কাজ এবং উত্তরের বাকি অংশের জন্য প্রতি টোকেনে বেশি মেমোরি রিড করা।
ব্যাচিং কেন থ্রুপুট বাড়ায় এবং টেইল ল্যাটেন্সি (tail latency) কমিয়ে দেয়?
ডিকোড (decode) প্রক্রিয়াটি ব্যান্ডউইথ-নির্ভর হওয়ায়, কম্পিউটেশনের দিক থেকে অতিরিক্ত রিকোয়েস্ট প্রসেস করা প্রায় বিনামূল্যে করা সম্ভব। ওয়েট (weights) একবার রিড করলেই ব্যাচের প্রতিটি সিকোয়েন্সের জন্য একটি করে টোকেন তৈরি করা যায়। ফলে, KV পুল শেষ না হওয়া পর্যন্ত অথবা ব্যাচটি পুনরায় কম্পিউট-নির্ভর হয়ে না ওঠা পর্যন্ত, ব্যাচের আকার বাড়ার সাথে সাথে মোট থ্রুপুট প্রায় রৈখিকভাবে বৃদ্ধি পায়। কন্টিনিউয়াস ব্যাচিং (continuous batching) প্রতি ধাপে ব্যাচটিকে নতুন করে সাজায়, ফলে কোনো রিকোয়েস্ট শেষ হলে সেটি বেরিয়ে যায় এবং নতুন রিকোয়েস্ট তার প্রতিবেশীদের জন্য অপেক্ষা না করেই যুক্ত হতে পারে।
এর প্রভাব মূলত পার্সেন্টাইলগুলোতে (percentiles) দেখা যায়। প্রতিটি ইউজারের পরবর্তী টোকেন এখন একটি শেয়ারড স্টেপের সবচেয়ে ধীরগতির অংশের জন্য অপেক্ষা করে। ফলে p50 বা মিডিয়ান ল্যাটেন্সি গ্রহণযোগ্য পর্যায়ে থাকলেও, p99 বা প্রতি 100টি রিকোয়েস্টের মধ্যে সবচেয়ে ধীরগতির 1টি রিকোয়েস্টের ল্যাটেন্সি বেড়ে যায়। ব্যবহারকারীরা মূলত p99-কেই অনুভব করেন, কারণ এটি বাক্যের মাঝখানে বিরতি হিসেবে ধরা দেয়।
প্রিফিল (prefill) এই সমস্যাটিকে আরও প্রকট করে তোলে। স্ট্রিম চলাকালীন বড় কোনো প্রম্পট আসলে তা ডিভাইসটিকে একটি দীর্ঘ সময়ের জন্য ব্যস্ত রাখে এবং বর্তমানে স্ট্রিম করা সবাই একটি বিরতি অনুভব করেন। চাঙ্কড প্রিফিল (chunked prefill) একটি বড় প্রম্পটকে ছোট ছোট টুকরোয় ভাগ করে এবং প্রতিটি টুকরোকে ডিকোড ব্যাচের সাথে মিশিয়ে এই সমস্যার বেশিরভাগ সমাধান করে। আগস্ট 2026 অনুযায়ী, vLLM V1 ইঞ্জিন ডিফল্টভাবে এটি করে এবং --max-num-batched-tokens-এর মাধ্যমে এই ভারসাম্য নিয়ন্ত্রণ করতে দেয়। vLLM টিউনিং ডকুমেন্টেশনে এই ট্রেড-অফটি স্পষ্টভাবে বলা হয়েছে: 2048-এর মতো ছোট মানগুলো ইন্টার-টোকেন ল্যাটেন্সি (ITL) উন্নত করে, কারণ তখন প্রিফিল ডিকোডকে কম বাধা দেয়। অন্যদিকে, বড় মানগুলো TTFT উন্নত করে, কারণ তখন একটি ব্যাচে বেশি প্রিফিল টোকেন জায়গা পায়। এই একটি ফ্ল্যাগই প্রিফিল বনাম ডিকোড-এর ভারসাম্য নির্ধারণ করে, যা আপনি প্রয়োজনমতো পরিবর্তন করতে পারেন। p99 ল্যাটেন্সি কখন আর গ্রহণযোগ্য থাকে না, তা মূলত ধারণক্ষমতার (capacity) বিষয়। একটি self-hosted LLM কতজন ব্যবহারকারীকে একসাথে সেবা দিতে পারে তা একই মেট্রিক্স ব্যবহার করে বিশ্লেষণ করা হয়েছে।
কেন বড় GPU ব্যবহার করলেও অনেক সময় কোনো পরিবর্তন হয় না?
কারণ বড় GPU মানে সাধারণত বেশি compute ক্ষমতা, আর decode প্রক্রিয়ার জন্য compute-এর প্রয়োজন হয় না।
উপরের তালিকার দুটি সারি তুলনা করুন। A100 80GB কার্ডের প্রকাশিত bandwidth হলো 2039 GB/s, যেখানে L40S কার্ডের bandwidth হলো 864 GB/s। decode-এর সর্বোচ্চ সীমাও ঠিক সেই অনুযায়ী পরিবর্তিত হয়: 127 tokens per second বনাম 54। RTX 4090 অধিকাংশ পরিমাপেই একটি অত্যন্ত দ্রুতগতির কার্ড এবং এর 1008 GB/s bandwidth এর সর্বোচ্চ সীমাকে 63-এ নিয়ে যায়। দুটি কার্ডের মধ্যে অন্য যা কিছুই পার্থক্য থাকুক না কেন, সিঙ্গেল স্ট্রিম decode সবসময় স্পেসিফিকেশন শিটে থাকা bandwidth-এর ওপর নির্ভর করে।
তাই decode-কে দ্রুত করার দুটি উপায় আছে: প্রতি token-এ কম byte পড়া (weights-কে quantize করা বা ছোট মডেল চালানো), অথবা বেশি bandwidth-এর কার্ড কেনা। Prefill-এর ক্ষেত্রে বিষয়টি উল্টো। Prefill-এর জন্য compute প্রয়োজন, তাই একটি দ্রুতগতির কার্ড দীর্ঘ prompt-এর ক্ষেত্রে TTFT (Time To First Token) কমিয়ে দেয়। যদি অভিযোগ থাকে যে প্রথম token আসতে চার সেকেন্ড সময় লাগছে, তবে উন্নত হার্ডওয়্যার তা সমাধান করতে পারে। কিন্তু যদি অভিযোগ থাকে যে টেক্সট ধীরে টাইপ হচ্ছে, তবে হার্ডওয়্যার পরিবর্তন করে খুব একটা লাভ হবে না।
আপনার কি prefill এবং decode আলাদা worker-এ চালানো উচিত?
বড় সার্ভিং স্ট্যাকগুলো ঠিক এই কাজটিই করে, এবং এই কৌশলটিকে বলা হয় prefill এবং decode disaggregation। এখানে worker-এর একটি পুল শুধুমাত্র prefill চালায়, দ্বিতীয় একটি পুল শুধুমাত্র decode চালায়, এবং প্রথম পুলের তৈরি করা KV cache একটি দ্রুতগতির ইন্টারকানেক্টের মাধ্যমে দ্বিতীয় পুলে স্থানান্তর করা হয়। এটি কার্যকর কারণ এই দুটি ধাপের জন্য ভিন্ন ভিন্ন হার্ডওয়্যার এবং শিডিউলিং প্রয়োজন। Prefill-এর জন্য প্রয়োজন কম্পিউট পাওয়ার এবং বড় টোকেন ব্যাচ। Decode-এর জন্য প্রয়োজন ব্যান্ডউইথ এবং অনেকগুলো সমসাময়িক সিকোয়েন্স। এদের আলাদা করার ফলে প্রতিটি পুল স্বাধীনভাবে স্কেল করতে পারে এবং একটি বিশাল প্রম্পট যেন অন্য সব সক্রিয় স্ট্রিমকে আটকে না দেয়, তা নিশ্চিত করা যায়।
একটি মাত্র GPU যুক্ত একটি VPS (virtual private server)-এ এটি করা প্রায় কখনোই লাভজনক নয়। আপনি একটি ডিভাইসকে নিজের বিরুদ্ধেই ভাগ করে ফেলবেন এবং একটি পয়েন্টারকে গিগাবাইট সাইজের ক্যাশ ট্রান্সফারে রূপান্তর করবেন। এই কৌশলটি তখনই লাভজনক হয় যখন আপনার কাছে প্রতিটি ধাপের জন্য আলাদা মেশিন বরাদ্দ করার মতো পর্যাপ্ত এক্সিলারেটর থাকে এবং উভয় পুলকে ব্যস্ত রাখার মতো যথেষ্ট নিয়মিত ট্রাফিক থাকে। এর চেয়ে কম ট্রাফিকের ক্ষেত্রে, chunked prefill ব্যবহার করলে একটি ফ্ল্যাগ দিয়েই প্রায় একই ধরনের আইসোলেশন পাওয়া সম্ভব।
সংখ্যাটি খারাপ হলে যা পরিবর্তন করতে হবে
যখন TTFT খুব বেশি হয়:
- প্রম্পট ছোট করুন। Prefill cost প্রম্পট টোকেনগুলোর হিসাব রাখে এবং সিস্টেম প্রম্পট প্রতিটি অনুরোধের জন্য চার্জ করা হয়।
- Prefix caching চালু করুন যাতে একটি পুনরাবৃত্তিমূলক প্রিফিক্স প্রতিবার গণনা না করে একবারই করা হয়।
--max-num-batched-tokensবৃদ্ধি করুন যাতে প্রতিটি ধাপে আরও বেশি prefill-এর কাজ সম্পন্ন হয়।- মডেলকে দোষারোপ করার আগে কিউ (queue) পরীক্ষা করুন।
vllm:num_requests_waitingশূন্যের উপরে থাকা মানে হলো অনুরোধটি শুরুই হয়নি, যা একটি ক্যাপাসিটি বা সক্ষমতার সমস্যা।
যখন প্রতি সেকেন্ডে টোকেনের সংখ্যা (tokens per second) খুব কম হয়:
- ওয়েটগুলো (weights) কোয়ান্টাইজ করুন। প্রতি ওয়েটে কম বাইট মানে প্রতি টোকেনে কম বাইট পড়া।
- আপনার কার্ডের প্রকাশিত মেমরি ব্যান্ডউইথ উপরের চার্টের সাথে মিলিয়ে দেখুন এবং আপনি সীমার কতটা কাছাকাছি আছেন তা যাচাই করুন।
--max-num-batched-tokensকমিয়ে দিন যাতে prefill-এর কারণে decode-এ কম বাধা সৃষ্টি হয়।- কনটেক্সট লেন্থ পরীক্ষা করুন। হাজার হাজার টোকেনে পরিণত হওয়া একটি কথোপকথন প্রতিটি ধাপে অনেক বড় KV cache পড়ে।
এখানে রানটাইমও গুরুত্বপূর্ণ, কারণ Ollama এবং vLLM প্রিফিল ও ডিকোড ভিন্নভাবে শিডিউল করে, এবং একটি সেটিং যা একটিতে কাজ করে তা অন্যটিতে কোনো প্রভাব নাও ফেলতে পারে। প্রথমে উভয় পর্যায়ে পরিমাপ করুন, তারপর একটি বিষয় পরিবর্তন করুন।
FAQ
আমার প্রথম টোকেন আসতে কয়েক সেকেন্ড সময় লাগে কিন্তু বাকিগুলো দ্রুত স্ট্রিম হয় কেন?
এই অপেক্ষার সময়টি হলো প্রিফিল (prefill), আর স্ট্রিম হওয়া অংশটি হলো ডিকোড (decode)। প্রিফিল পুরো প্রম্পটটিকে একটি কম্পিউট-বাউন্ড ধাপে প্রসেস করে, তাই আউটপুট আসার আগেই প্রম্পটের দৈর্ঘ্যের ওপর ভিত্তি করে এর সময় বাড়ে। এরপর ডিকোড প্রতি ধাপে একটি করে টোকেন তৈরি করে, যার গতি নির্ভর করে মেমোরি ব্যান্ডউইথের ওপর; এটি প্রম্পট কত বড় ছিল তার ওপর প্রায় নির্ভরশীল নয়। প্রতিটি রিকোয়েস্টে দীর্ঘ সিস্টেম প্রম্পট ব্যবহার করা এর সাধারণ কারণ। প্রিফিক্স ক্যাশিং (prefix caching) এই পুনরাবৃত্তিমূলক খরচের অংশটি কমিয়ে দেয়।
প্রম্পট বড় হলে কি প্রতি সেকেন্ডে টোকেনের গতি কমে যায়?
সামান্য কমে, তবে এর কারণ TTFT-এর চেয়ে ভিন্ন। প্রতিটি ডিকোড ধাপ আগের সব টোকেনের কি (keys) এবং ভ্যালু (values) পড়ে, তাই বড় KV ক্যাশের অর্থ হলো প্রতি টোকেনে বেশি বাইট পড়া। সাধারণ 8 বিলিয়ন প্যারামিটার লেআউটের জন্য ক্যাশ প্রতি টোকেনে প্রায় 128 KiB হয়, তাই 8,000 টোকেন কনটেক্সটের ক্ষেত্রে প্রতি ধাপে প্রায় 1 GB ডেটা প্রসেস করতে হয়। দীর্ঘ প্রম্পটের বড় প্রভাবটি এখনো TTFT-এর ওপরই থাকে, স্ট্রিমিং গতির ওপর নয়।
কোন GPU স্পেসিফিকেশন ডিকোড গতি নির্দেশ করে?
মেমোরি ব্যান্ডউইথ। প্রকাশিত ব্যান্ডউইথকে মেমোরিতে থাকা ওয়েট (weights)-এর আকার দিয়ে ভাগ করলে আপনি একটি স্ট্রিমের গাণিতিক সর্বোচ্চ সীমা পাবেন। বেশি কম্পিউট ক্ষমতা কিন্তু একই ব্যান্ডউইথ আছে এমন কার্ডে স্ট্রিমিং দ্রুত হবে না। এই কারণেই 8 বিটে কোয়ান্টাইজ (quantize) করলে ডিকোড গতি প্রায় দ্বিগুণ হয়: এটি কম্পিউট ক্ষমতা পরিবর্তন না করেই প্রতি টোকেনে পড়া বাইটের সংখ্যা অর্ধেক করে দেয়।
ব্যবহারকারী বাড়লে থ্রুপুট কেন বাড়ে কিন্তু প্রতিটি ব্যবহারকারীর কাছে কেন ধীর মনে হয়?
ওয়েটগুলো একবার পড়লে তা ব্যাচের প্রতিটি সিকোয়েন্সের জন্য টোকেন তৈরি করতে পারে, তাই ব্যাচের আকার বাড়লে প্রতি সেকেন্ডে মোট টোকেনের সংখ্যা বাড়ে। প্রতিটি টোকেন এখন একটি শেয়ার্ড ধাপের জন্য অপেক্ষা করে, তাই একই সময়ে প্রতি ব্যবহারকারীর ল্যাটেন্সি (latency) বেড়ে যায়। সামগ্রিক থ্রুপুট সংখ্যার দিকে না তাকিয়ে p99 ইন্টার-টোকেন ল্যাটেন্সি পর্যবেক্ষণ করুন এবং রিকোয়েস্টগুলো রান না করে কিউতে (queueing) আটকে আছে কি না তা দেখতে vllm:num_requests_waiting চেক করুন।