কেন 5 জন ব্যবহারকারীর পর self-hosted LLM ধীর হয়ে যায়?
Ollama-এর ডিফল্ট num_parallel মান 1 হওয়ায় একাধিক ব্যবহারকারী যোগ দিলে সার্ভার ধীর হয়ে যায়। KV cache, batching এবং prefill সীমাবদ্ধতা কীভাবে আপনার LLM-এর গতি কমিয়ে দেয় তা জানুন।
একাধিক ব্যবহারকারী যুক্ত হলে self-hosted LLM কেন ধীর হয়ে যায়?
একটি self-hosted LLM 5 জন ব্যবহারকারীর ক্ষেত্রে ধীর হয়ে যায় কারণ সার্ভারটি তখনও একবারে একটি মাত্র উত্তর তৈরি করছে, এবং বাকি চারজন সারিবদ্ধভাবে অপেক্ষা করছে। Ollama-এর ডকুমেন্টেশনে ডিফল্ট সেটিংস সম্পর্কে সরাসরি বলা হয়েছে: OLLAMA_NUM_PARALLEL হলো "প্রতিটি মডেল একই সময়ে সর্বোচ্চ যতগুলো সমান্তরাল অনুরোধ প্রসেস করবে, যার ডিফল্ট মান 1।" এখানে কোনো কিছু নষ্ট হয়নি। আপনার পাঁচজন ব্যবহারকারীর মধ্যে চারজনই তাদের সুযোগের জন্য অপেক্ষা করছে।
এর সমাধান সাধারণত আরও শক্তিশালী হার্ডওয়্যার নয়। এর সমাধান হলো এমন একটি serving engine যা একই forward pass-এ অনেকগুলো অনুরোধ মডেলের মধ্য দিয়ে পাঠাতে পারে, এবং সেই সাথে পর্যাপ্ত অতিরিক্ত মেমোরি রাখা যা এই প্রক্রিয়ার সময় সবার কথোপকথন ধরে রাখতে পারে। এই দুটি অংশই গুরুত্বপূর্ণ, এবং দ্বিতীয় অংশটিই মূলত আপনার সক্ষমতার সীমা নির্ধারণ করে।
প্রতিটি অনুরোধ যে দুটি ধাপের মধ্য দিয়ে যায়
Prefill পুরো প্রম্পটটি একসাথে পড়ে এবং এর জন্য attention cache তৈরি করে। প্রতিটি প্রম্পট টোকেন মডেলের মধ্য দিয়ে একসাথে যায়, তাই prefill হলো একটি বড় matrix multiply, যা arithmetic throughput দ্বারা সীমাবদ্ধ। অন্যদিকে, Decode উত্তরটি একটি একটি করে টোকেন তৈরি করে। প্রতিটি টোকেনের জন্য মডেলের সম্পূর্ণ weights মেমোরি থেকে পুনরায় পড়তে হয়, যদিও সেই একটি টোকেনের জন্য প্রয়োজনীয় গাণিতিক হিসাব খুবই সামান্য। Decode মূলত memory bandwidth দ্বারা সীমাবদ্ধ।
এই অসামঞ্জস্যই হলো batching কার্যকর হওয়ার মূল কারণ। একজন ব্যবহারকারীর জন্য ডিকোডিং করার সময় প্রতি টোকেনে প্রায় 5 GB weights পড়তে হয় এবং বেশিরভাগ arithmetic unit অলস বসে থাকে। দ্বিতীয় একটি অনুরোধ যোগ করলে ইঞ্জিন একই 5 GB একবার পড়ে এবং তা থেকে দুটি টোকেন হিসাব করে। দ্বিতীয় ব্যবহারকারীর জন্য প্রায় কোনো অতিরিক্ত সময়ের প্রয়োজন হয় না। অনুরোধগুলোকে একের পর এক সিরিয়াল অনুযায়ী সার্ভ করলে এই সুবিধাটি নষ্ট হয়ে যায়।
ব্যবহারকারীর অভিজ্ঞতা দুটি সংখ্যার মাধ্যমে বোঝা যায়। TTFT (time to first token) হলো কিউতে অপেক্ষার সময় এবং prefill-এর যোগফল। ITL (inter-token latency) হলো স্ট্রিম করা টোকেনগুলোর মধ্যবর্তী বিরতি, যা decode দ্বারা নির্ধারিত হয়। একটি ধীরগতির সার্ভার সাধারণত এই দুটির যেকোনো একটির কারণে ধীর হয় এবং এর সমাধানগুলো ভিন্ন। কোনো সেটিং পরিবর্তন করার আগে আপনি ঠিক কোনটির সাথে লড়াই করছেন তা নিশ্চিত হওয়া জরুরি, এবং prefill এবং decode-এর সময় আলাদাভাবে পরিমাপ করা হলো এটি খুঁজে বের করার উপায়।
Static batching-এর কারণে সবাইকে সবচেয়ে ধীরগতির উত্তরের জন্য অপেক্ষা করতে হয়
Static batching হলো এর প্রাথমিক সংস্করণ, যা আপনি অ্যাপ্লিকেশন কোডে নিজেই রিকোয়েস্টগুলো গ্রুপ করলে পাবেন। ইঞ্জিন N সংখ্যক রিকোয়েস্ট সংগ্রহ করে, সেগুলোকে একসাথে চালায় এবং গ্রুপের সবচেয়ে দীর্ঘ জেনারেশন শেষ না হওয়া পর্যন্ত প্রতিটি স্লট আটকে রাখে।
একজন ব্যবহারকারী 1,200 টোকেনের একটি সামারি চাইলে তা চারটি এক লাইনের উত্তরকে ব্যাচের মধ্যে আটকে রাখে, কারণ ব্যাচটি তার সবচেয়ে ধীরগতির সদস্যের কাজ শেষ না হওয়া পর্যন্ত কোনো স্লট মুক্ত করে না।
এর ফলে দুটি খরচ বাড়ে। সম্পন্ন হওয়া সিকোয়েন্সগুলো এমন স্লট দখল করে রাখে যা কোনো কার্যকর কম্পিউটেশন করছে না, তাই আউটপুটের দৈর্ঘ্য ভিন্ন হওয়ার সাথে সাথে কার্যকর থ্রুপুট কমে যায়, আর চ্যাট আউটপুটের দৈর্ঘ্য অনেক বেশি পরিবর্তিত হয়। ব্যাচ তৈরি হওয়ার ঠিক এক ধাপ পরে আসা একটি রিকোয়েস্টকে পুরো ব্যাচ শেষ না হওয়া পর্যন্ত অপেক্ষা করতে হয়, এমনকি প্রিফিল (prefill) শুরু করার জন্যও। এর মানে হলো, রিকোয়েস্টটির TTFT অন্য কারো লেখা দীর্ঘ উত্তরের ওপর নির্ভর করে নির্ধারিত হয়।
Continuous batching প্রতিটি টোকেনের ভিত্তিতে অনুরোধ গ্রহণ ও সম্পন্ন করে
Continuous batching প্রতিটি decoding ধাপের স্তরে শিডিউলিং করে। প্রতিটি ধাপের পর শিডিউলার সেই সিকোয়েন্সগুলোকে বাদ দেয় যেগুলোর stop token জেনারেট করা শেষ হয়েছে, এরপর খালি স্লটগুলোতে অপেক্ষমাণ নতুন অনুরোধগুলোকে যুক্ত করে। কোনো রিপ্লাই যদি 40 নম্বর ধাপে শেষ হয়, তবে সেটি 40 নম্বর ধাপেই তার স্লট খালি করে দেয়, পুরো ব্যাচ শেষ হওয়ার জন্য অপেক্ষা করে না।
এটি কোনো অস্বাভাবিক বিষয় নয়। llama-server-এ -cb, --cont-batching-কে "continuous batching (যাকে dynamic batching-ও বলা হয়) সক্রিয় করা হবে কি না (ডিফল্ট: সক্রিয়)" হিসেবে নথিবদ্ধ করা হয়েছে এবং vLLM এই ধারণার ওপর ভিত্তি করেই তৈরি। Ollama-ও সমান্তরাল অনুরোধ সার্ভ করতে পারে। ডিফল্ট কনফিগারেশনে এই সংখ্যা 1-এ সীমাবদ্ধ থাকে, আর এই কারণেই অনেকে মনে করেন তাদের হার্ডওয়্যার কনকারেন্সি সাপোর্ট করে না, অথচ মূলত তাদের কনফিগারেশনেই এটি বন্ধ করা থাকে।
Continuous batching-এর প্রকাশিত ফলাফলগুলো সাধারণত ডেটাসেন্টার কার্ডে পরিমাপ করা হয়, যেগুলোতে অতিরিক্ত কম্পিউট পাওয়ার এবং ক্যাশের জন্য কয়েক ডজন গিগাবাইট মেমোরি থাকে। সেই ফলাফলের ধরন আপনার মেশিনের ক্ষেত্রেও প্রযোজ্য, কিন্তু সেগুলোর আকার বা স্কেল এক নয়, আর এর কারণ নিচে মেমোরি সেকশনে ব্যাখ্যা করা হয়েছে।
Prefill এবং decode একই compute রিসোর্সের জন্য প্রতিযোগিতা করে
যখন চারটি রিপ্লাই স্ট্রিমিং অবস্থায় থাকে এবং নতুন একটি রিকোয়েস্ট আসে, তখন সেটির প্রম্পটকে প্রথমে prefill করতে হয়, আর prefill প্রচুর compute ব্যবহার করে। যদি শিডিউলার এই prefill-কে আলাদা একটি ধাপ হিসেবে গণ্য করে, তবে সেই সময়ে স্ট্রিমিং করা চারজন ব্যবহারকারী কোনো টোকেন পান না। দীর্ঘ প্রম্পটের ক্ষেত্রে প্রতিটি ওপেন উইন্ডোতে এটি একটি দৃশ্যমান বিরতি তৈরি করে। সার্ভারে অন্য কেউ কিছু পাঠানোর সাথে সাথে যে 'hiccup' বা হেঁচকি ওঠার কথা বলা হয়, এটিই সেই stutter।
Chunked prefill একটি দীর্ঘ প্রম্পটকে ছোট ছোট অংশে ভাগ করে এবং প্রতিটি অংশকে চলমান decode-এর সাথে একই ধাপে মিশ্রিত করে। vLLM-এর টিউনিং গাইড এই ট্রেড-অফটি স্পষ্টভাবে উল্লেখ করেছে: ছোট chunk budget "ভালো ITL নিশ্চিত করে কারণ এতে decode-কে ধীর করে দেয় এমন prefill-এর সংখ্যা কমে", অন্যদিকে বড় মানগুলো "ভালো time to first token (TTFT) দেয় কারণ আপনি একটি ব্যাচে আরও বেশি prefill টোকেন প্রসেস করতে পারেন"। আপনি কার অভিজ্ঞতাকে প্রাধান্য দেবেন তা আপনাকে বেছে নিতে হবে: যে ব্যক্তি উত্তরের অপেক্ষায় আছেন, নাকি যারা টেক্সট স্ট্রিম হতে দেখছেন।
প্রম্পটের দৈর্ঘ্য নির্ধারণ করে এই সমস্যাটি কতটা প্রকট হবে। 6,000 টোকেনের একটি প্রম্পট যার উত্তর 200 টোকেনের, সেখানে 200টি decode ধাপের বিপরীতে 6,000 টোকেনের prefill কাজ করতে হয়। Retrieval-augmented chat এবং দীর্ঘ সিস্টেম প্রম্পট উভয়ই আপনাকে এই অবস্থায় নিয়ে যায়, ফলে prefill তখন আর সামান্য কোনো বিষয় থাকে না, বরং ব্যবহারকারীরা যেটির জন্য অপেক্ষা করেন সেটিই হয়ে দাঁড়ায়। দীর্ঘ প্রম্পটের অংশগুলো বারবার ব্যবহৃত হলে Prefix caching সাহায্য করে: vLLM --enable-prefix-caching সুবিধাটি প্রদান করে, যা প্রতিটি রিকোয়েস্টের জন্য পুনরায় গণনা না করে একটি শেয়ার্ড প্রম্পট প্রিফিক্সের জন্য ক্যাশ পুনরায় ব্যবহার করে।
সবচেয়ে আগে যে মেমোরি ফুরিয়ে যায় তা হলো KV cache
প্রতিটি সক্রিয় কথোপকথনের প্রতিটি টোকেন মডেলের প্রতিটি লেয়ারে একটি key vector এবং একটি value vector রেখে যায়। একেই বলা হয় KV cache (key/value cache), এবং এটিই decode প্রক্রিয়াকে প্রতিটি নতুন টোকেনের জন্য পুরো প্রম্পট পুনরায় গণনা করা থেকে বিরত রাখে। প্রতি টোকেনে এর আকার মডেলের গঠনের ওপর নির্ভর করে: 2 (একটি key, একটি value) গুণিতক লেয়ারের সংখ্যা, গুণিতক key/value head-এর সংখ্যা, গুণিতক head dimension, গুণিতক প্রতি value-র বাইট সংখ্যা। এই সংখ্যাগুলো মডেলের config.json থেকে দেখে নিন।
একবার হিসাব করলেই মেমোরি সীমার রহস্য পরিষ্কার হয়ে যাবে। 36টি লেয়ার, 8টি key/value head এবং 128 head dimension বিশিষ্ট একটি সাধারণ 8B মডেলের ক্ষেত্রে, 16-বিট ফরম্যাটে cache ধরে রাখলে প্রতি টোকেনে খরচ হয় 2 36 8 128 2 বাইট। অর্থাৎ 147,456 বাইট বা প্রায় 144 KiB। সুতরাং একটি 8,192 টোকেনের কথোপকথনের জন্য প্রায় 1.2 GB cache প্রয়োজন। পাঁচটি কথোপকথনের জন্য মডেলের ওজনের (weights) বাইরেও প্রায় 6 GB মেমোরি প্রয়োজন, এবং কতজন ব্যবহারকারী যুক্ত হতে পারবেন তার আসল উত্তর এখানেই।
Concurrency বা একই সাথে একাধিক অনুরোধ context-এর আকার বাড়িয়ে দেয় এবং টুলগুলো স্পষ্টভাবে তা জানিয়ে দেয়। Ollama-এর FAQ অনুযায়ী: "একটি নির্দিষ্ট মডেলের জন্য সমান্তরাল অনুরোধ প্রক্রিয়াকরণ করলে সমান্তরাল অনুরোধের সংখ্যা অনুযায়ী context-এর আকার বৃদ্ধি পায়। উদাহরণস্বরূপ, 4টি সমান্তরাল অনুরোধসহ 2K context-এর ফলে 8K context এবং অতিরিক্ত মেমোরি বরাদ্দ হবে।" প্রয়োজনীয় RAM-এর পরিমাণ OLLAMA_NUM_PARALLEL এবং OLLAMA_CONTEXT_LENGTH-এর গুণফলের ওপর নির্ভর করে। llama-server-এ, -c দিয়ে আপনি যে context-এর অনুরোধ করেন তা -np স্লটগুলোর মধ্যে ভাগ হয়ে যায়, তাই শুধুমাত্র স্লটের সংখ্যা বাড়ালে প্রতিটি অনুরোধের জন্য বরাদ্দকৃত মেমোরি কমে যায়। তাই অনুমান না করে startup log থেকে প্রতি স্লটের context-এর পরিমাণ দেখে নিন।
vLLM এর পরিবর্তে আগে থেকেই মেমোরি বরাদ্দ (preallocate) করে রাখে। --gpu-memory-utilization (ডিফল্ট 0.92) হলো "মডেল executor-এর জন্য ব্যবহৃত GPU মেমোরির ভগ্নাংশ"। মডেলের ওজনের (weights) পর যা অবশিষ্ট থাকে তা paged KV pool হিসেবে ব্যবহৃত হয়, এবং যখন এই pool ফুরিয়ে যায়, তখন scheduler অনুরোধটি ব্যর্থ না করে বরং বাতিল (evict) করে দেয়:
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.vLLM-এর V1 ইঞ্জিনে ডিফল্ট preemption মোড হলো RECOMPUTE, তাই একটি বাতিল হওয়া অনুরোধ তার cache মুছে ফেলে এবং পুনরায় যুক্ত হওয়ার সময় আবার prefill করে। এই কাজটি দুইবার সম্পন্ন হয়। ডকুমেন্টেশনে সতর্ক করা হয়েছে যে "preemption এবং recomputation সামগ্রিক latency-র ওপর নেতিবাচক প্রভাব ফেলতে পারে", এবং এই লগ লাইনটিই সবচেয়ে ভালো ব্যাখ্যা দেয় যে কেন একজন দুর্ভাগ্যবান ব্যবহারকারী অন্যদের চেয়ে অনেক বেশি সময় অপেক্ষা করেছেন, যদিও আপনার গড় পারফরম্যান্স স্বাভাবিক দেখাচ্ছিল। ক্রমবর্ধমান সংখ্যাটি লগ করতে disable_log_stats=False সেট করুন, অথবা vLLM যে Prometheus metrics প্রকাশ করে সেখান থেকে preemption counter দেখে নিন।
2, 5 এবং 20 জন কনকারেন্ট ব্যবহারকারীর ক্ষেত্রে কী পরিবর্তন হয়
দুইজন ব্যবহারকারী। ক্যাশ মেমোরি পর্যাপ্ত থাকলে একটি GPU-তে এটি প্রায় অদৃশ্য, কারণ দ্বিতীয় ডিকোড স্ট্রিমটি প্রথমটির সাথে খুব সামান্য বাড়তি সময়েই সম্পন্ন হয়। 4 থেকে 8 GB RAM-এর একটি CPU-only VPS-এ এটি বিনামূল্যে হয় না: উভয় স্ট্রিম একই সংখ্যক vCPU এবং RAM ব্যান্ডউইথ শেয়ার করে, তাই প্রতিটি ব্যবহারকারী প্রতি সেকেন্ডে প্রায় অর্ধেক টোকেন পায় এবং অনেক কম বাজেটের বিপরীতে ক্যাশের চাহিদা দ্বিগুণ হয়ে যায়।
পাঁচজন ব্যবহারকারী। এই পর্যায়ে ডিফল্ট সেটিংস আর যথেষ্ট থাকে না এবং এটি একটি কিউ (queue) সমস্যা হিসেবে দেখা দেয়। OLLAMA_NUM_PARALLEL 1-এ সেট করা থাকলে, চারজন ব্যবহারকারীকে অপেক্ষা করতে হয় যে ব্যক্তি দীর্ঘ উত্তরের জন্য অনুরোধ করেছে তার জন্য, এবং তাদের পালা আসার সাথে সাথেই তারা স্বাভাবিক গতি পায়। প্যারালাল কাউন্ট বা সমান্তরাল সংখ্যা বাড়ালে সমস্যার ধরন বদলে যায়: প্রতিটি 8K কনটেক্সট হিসেবে পাঁচটি স্লটের জন্য 40K টোকেন ক্যাশ খুঁজে বের করতে হয়। যদি এটি VRAM-এ না ধরে, তবে ইঞ্জিন লেয়ারগুলোকে সিস্টেম RAM-এ অফলোড করে, আর যদি RAM-এও না ধরে, তবে সিস্টেম সোয়াপ (swap) করতে শুরু করে এবং প্রতি সেকেন্ডে টোকেন পাওয়ার হার ধসে পড়ে।
বিশজন ব্যবহারকারী। চ্যাট UI-তে বিশজন মানুষ সাধারণত বিশটি কনকারেন্ট রিকোয়েস্ট নয়, এবং হার্ডওয়্যার কেনার আগে এটি বোঝা সবচেয়ে জরুরি। একজন মানুষ উত্তর পড়ার সময় এবং পরবর্তী উত্তরের কথা ভাবার জন্য 20 থেকে 60 সেকেন্ড সময় নেয়, তাই তাদের সেশনের বেশিরভাগ সময় অলস (idle) থাকে। বিশজন এজেন্ট বা বিশটি ডকুমেন্ট সামারাইজেশন জব হলো বিশটি প্রকৃত স্ট্রিম যেখানে কোনো অলস সময় থাকে না। এটি সম্পূর্ণ ভিন্ন একটি মেশিনের কাজ। একজন ডেভেলপার যিনি নিজের Ollama সার্ভারে একটি কোডিং এজেন্ট যুক্ত করেছেন, তিনি প্রথম ক্ষেত্রের চেয়ে দ্বিতীয় ক্ষেত্রের কাছাকাছি থাকেন, কারণ যতক্ষণ টাস্কটি চলে ততক্ষণ এজেন্ট ক্রমাগত রিকোয়েস্ট পাঠাতে থাকে এবং মানুষের মতো পড়ার জন্য কোনো বিরতি নেয় না।
আপনার ব্যবহারকারীরা কি একই সময়ে সক্রিয়, নাকি কেবল লগ-ইন করা?
যেকোনো কিছুর আকার নির্ধারণের আগে কতগুলো অনুরোধ একসাথে প্রসেস হচ্ছে (requests in flight) তা হিসাব করুন। গাণিতিক হিসাবটি সাধারণ: একসাথে সক্রিয় অনুরোধের সংখ্যা হলো ব্যবহারকারীর সংখ্যা, গুণ প্রতি টার্নে জেনারেশনের জন্য ব্যয় করা সেকেন্ড, ভাগ প্রতি টার্নের মধ্যবর্তী সেকেন্ড।
- প্রথমে আপনার নিজের সিঙ্গেল-স্ট্রিম গতি পরিমাপ করুন, প্রিফিল (prefill) এবং ডিকোড (decode) উভয়ই। অন্য কারো কার্ডের সংখ্যা ধার নেবেন না: আপনার নিজের মেশিনে প্রতি সেকেন্ডে কতগুলো টোকেন প্রসেস হচ্ছে তা পরিমাপ করুন এবং প্রাপ্ত ফলাফল ব্যবহার করুন।
- ডিউটি সাইকেল (duty cycle) অনুমান করুন। বিশজন চ্যাট ব্যবহারকারী, প্রতি টার্নে 12 সেকেন্ড জেনারেশন, প্রতি 90 সেকেন্ডে একটি টার্ন হলে হিসাবটি দাঁড়ায় 20 * 12 / 90, অর্থাৎ প্রায় 2.7টি অনুরোধ একসাথে প্রসেস হচ্ছে।
- স্লট সংখ্যা এর চেয়ে সামান্য বেশি নির্ধারণ করুন, তারপর মেমরির সাথে মিলিয়ে দেখুন: স্লট সংখ্যা এবং প্রতি অনুরোধের কনটেক্সট গুণ করলে তা অবশ্যই আপনার কাছে থাকা ক্যাশ টোকেনের মধ্যে থাকতে হবে।
- কিউ (queue) ছোট রাখুন যাতে ওভারফ্লো হলে তা দ্রুত এবং স্পষ্টভাবে ধরা পড়ে।
ক্যাশ টোকেন হলো ওয়েট (weights) লোড করার পর অবশিষ্ট ফ্রি মেমরি, যাকে উপরের সেকশনে উল্লিখিত প্রতি-টোকেন খরচ দিয়ে ভাগ করতে হয়। একটি 24 জিবি কার্ডে 16-বিটে 8বি মডেল চালালে ওয়েটের জন্য প্রায় 16 জিবি খরচ হয় এবং ডিফল্ট ইউটিলাইজেশনে প্রায় 6 জিবি ক্যাশ পাওয়া যায়, যা প্রায় পাঁচটি 8কে কনভারসেশনের সমান। আরও বেশি কনভারসেশন রাখতে চাইলে প্রতি অনুরোধের কনটেক্সট ছোট করুন অথবা 8-বিটে ক্যাশ সংরক্ষণ করুন (llama-server এর জন্য --cache-type-k q8_0 প্রয়োজন)। উভয় পদ্ধতিই কিছু ছাড় দিয়ে কনকারেন্সি (concurrency) বাড়ায়, এবং হার্ডওয়্যারে অর্থ বিনিয়োগের আগে এই ট্রেড-অফের বিষয়টি জেনে নেওয়া বুদ্ধিমানের কাজ: GPU VPS বনাম API টোকেনের খরচের তুলনা।
যেখানে Ollama-এর ডিফল্ট সেটিংস যথেষ্ট নয়
সার্ভিস ইউনিটের মাধ্যমে প্যারালাল কাউন্ট বা সমান্তরাল অনুরোধের সংখ্যা বৃদ্ধি করুন, কারণ শেল এক্সপোর্ট systemd দ্বারা পরিচালিত ডেমনের কাছে পৌঁছাবে না।
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show কমান্ডটি আপনার সেট করা তিনটি ভেরিয়েবল প্রদর্শন করবে। যদি তা না দেখায়, তবে ড্রপ-ইন ফাইলটি সেভ হয়নি এবং পরবর্তী কোনো পদক্ষেপই কার্যকর হবে না। ollama ps কমান্ডটি লোড করা মডেলের আকার দেখাবে, যা শুধুমাত্র ওয়েট (weights) ফাইলের চেয়ে বড় হবে; কারণ 8,192 টোকেনের চারটি স্লট অতিরিক্ত 32,768 টোকেনের ক্যাশ রিজার্ভ করে রাখে। PROCESSOR কলামে যদি মডেলের কিছু অংশ CPU-তে দেখায় অথচ আপনি পুরোটা GPU-তে আশা করেছিলেন, তবে এর অর্থ হলো আপনি কার্ডের ধারণক্ষমতার চেয়ে বেশি ক্যাশ চেয়েছেন। এই দুটি সংখ্যার যেকোনো একটি কমিয়ে দিন। সাধারণত কনটেক্সট কমানো নিরাপদ, কিন্তু উইন্ডো খুব ছোট হলে তা কোনো এরর না দেখিয়েই দীর্ঘ প্রম্পটগুলোকে কেটে ফেলে। তাই num_ctx-এর আকার সচেতনভাবে নির্ধারণ করা ভালো, শুধুমাত্র মডেল ফিট করার জন্য এটি কমিয়ে দেওয়া উচিত নয়।
কিউ (queue) ডিফল্ট সেটিংসটি পুনরায় বিবেচনা করা প্রয়োজন। Ollama সর্বোচ্চ OLLAMA_MAX_QUEUE টি অনুরোধ কিউতে রাখতে পারে এবং "ডিফল্ট মান হলো 512"। এর বেশি হলে সার্ভার "503 error" প্রদান করে, যা নির্দেশ করে যে সার্ভারটি ওভারলোডেড। যে সার্ভার একসাথে চারটি অনুরোধ প্রসেস করে, সেখানে 512টি অনুরোধের কিউ রাখা একটি অবাস্তব প্রতিশ্রুতি, কারণ 300 নম্বর অবস্থানে থাকা ক্লায়েন্ট তার সুযোগ আসার অনেক আগেই টাইম-আউট হয়ে যাবে। কিউ ছোট রাখলে সার্ভার দ্রুত এরর প্রদান করে, যা আপনার অ্যাপ্লিকেশনকে পুনরায় চেষ্টা (retry) করতে বা রিপোর্ট করতে সাহায্য করে; এটি এমন একটি প্রসেসের চেয়ে ভালো যা কখনোই শেষ হয় না।
বাস্তব পরীক্ষা করুন। দুটি আলাদা টার্মিনাল থেকে একই সময়ে দুটি অনুরোধ পাঠান এবং উভয়ই পর্যবেক্ষণ করুন। যদি প্রথমটি শেষ না হওয়া পর্যন্ত দ্বিতীয়টি কোনো আউটপুট না দেয়, তবে বুঝতে হবে প্যারালাল সেটিংস কার্যকর হয়নি।
কখন একটি রিয়েল সার্ভিং ইঞ্জিন নিজের খরচ তুলে আনে
আপনার কাছে যদি পর্যাপ্ত GPU headroom থাকে এবং একসাথে চারটির বেশি অনুরোধ প্রসেস করার প্রয়োজন হয়, তবেই vLLM ব্যবহারের বাড়তি পরিশ্রম সার্থক হয়। এর শিডিউলার প্রতিটি টোকেন ধরে কাজ করে, এর ক্যাশ পেজড (paged) হওয়ায় খালি অংশগুলো পুনরায় ব্যবহার করা যায় এবং এটি অব্যবহৃত VRAM-কে অলস ফেলে না রেখে কনকারেন্সিতে (concurrency) রূপান্তর করে। আগস্ট 2026 অনুযায়ী, এর ইন্সটল এবং লঞ্চ করার প্রক্রিয়া মাত্র দুটি কমান্ডের মাধ্যমে সম্পন্ন হয়:
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'একটি choices অ্যারে সম্বলিত রিপ্লাই মানে হলো সার্ভার চালু আছে এবং মডেলটি লোড হয়েছে। লোড চলাকালীন দুটি গুরুত্বপূর্ণ সেটিংস হলো --max-num-seqs, যা "একটি ইটারেশনে প্রসেস করা সিকোয়েন্সের সর্বোচ্চ সংখ্যা" নির্ধারণ করে এবং --max-num-batched-tokens, যা "একটি ইটারেশনে প্রসেস করা টোকেনের সর্বোচ্চ সংখ্যা" নির্ধারণ করে। প্রথমটি কনকারেন্সির সীমা নির্ধারণ করে, আর দ্বিতীয়টি পূর্বোল্লিখিত চাঙ্কড প্রিফিল (chunked prefill) বাজেট নিয়ন্ত্রণ করে।
একসাথে চারটি অনুরোধের কম থাকলে অথবা সমর্থিত GPU নেই এমন কোনো মেশিনে vLLM ব্যবহার করলে জটিলতা বাড়লেও তেমন কোনো সুবিধা পাওয়া যায় না। এটি একটি CUDA-ক্লাস কার্ড প্রত্যাশা করে এবং শুরুর সময়ই বেশিরভাগ মেমোরি দখল করে নেয়, যা 4 থেকে 8 GB RAM-এর VPS-এর জন্য উপযুক্ত নয়। সেক্ষেত্রে ছোট মডেল, কম কনটেক্সট এবং আপনার নিজের নিয়ন্ত্রণাধীন একটি কিউ (queue) ব্যবহার করাই ভালো। সার্ভিং ইঞ্জিন হিসেবে Ollama এবং vLLM-এর পার্থক্য এই নির্বাচনের বিষয়টি বিস্তারিত আলোচনা করে এবং একটি VPS-এ Qwen 3 8B চালানো দেখায় যে, একজন বাড়তি ব্যবহারকারী যোগ করার আগেই একটি মাঝারি আকারের মডেলের কী পরিমাণ রিসোর্স প্রয়োজন হয়।
লোককথায় যে বিনিময় বা ট্রেড-অফ এড়িয়ে যাওয়া হয়
Continuous batching মোট throughput বৃদ্ধি করে এবং সাধারণত median latency-ও উন্নত করে, কারণ একটি queued request দ্রুত শুরু হতে পারে। তবে tail latency-র ক্ষেত্রে উল্টোটি ঘটে এবং এই বিষয়টি খুব কমই আলোচিত হয়।
একটি step-এ প্রতিটি অতিরিক্ত sequence সামান্য বাড়তি কাজের চাপ তৈরি করে, তাই batch পূর্ণ হওয়ার সাথে সাথে সবার জন্য ITL বৃদ্ধি পায়। নতুন কোনো request-এর prefill এমন একটি step-এর অংশ দখল করে নেয়, যা অন্যথায় streaming ব্যবহারকারীদের জন্য বরাদ্দ থাকত। Cache-এর ওপর চাপ বাড়লে scheduler preempt করে, যার ফলে অর্ধেক তৈরি হওয়া একটি request পুনরায় তার prefill-এর শুরুতে ফিরে যায়।
একটি chat UI গড় মানের পরিবর্তে tail latency প্রদর্শন করে। কোনো stream যদি বাক্যের মাঝখানে দুই সেকেন্ডের জন্য থেমে যায়, তবে মোট completion time ভালো হলেও ব্যবহারকারীর কাছে তা ত্রুটিপূর্ণ মনে হয়। আপনার প্রত্যাশিত load-এর অধীনে p95 TTFT এবং p95 ITL পরিমাপ করুন এবং mean tokens per second-কে অভিজ্ঞতার বর্ণনা হিসেবে না দেখে বরং capacity-র সূচক হিসেবে বিবেচনা করুন।
এর থেকে ব্যবহারিক সেটিংস নির্ধারিত হয়। Concurrency-কে memory-র অনুমোদিত সীমার সামান্য নিচে রাখুন, যাতে engine-কে কখনোই preempt করতে না হয়। একটি ছোট এবং অনুমানযোগ্য queue এমন বড় batch-এর চেয়ে ভালো যা thrashing তৈরি করে; কারণ যে ব্যবহারকারী চার সেকেন্ড অপেক্ষা করার পর নির্বিঘ্নে stream করতে পারেন, তিনি সেই ব্যবহারকারীর চেয়ে বেশি সন্তুষ্ট থাকেন যিনি তাৎক্ষণিকভাবে শুরু করার পরও দুবার বাধার সম্মুখীন হন।
ধীরগতির ক্ষেত্রে যা পরীক্ষা করবেন
প্রতিটি ব্যবহারকারী স্বাভাবিক, কিন্তু অপেক্ষা দীর্ঘ। এটি একটি কিউ (queue) সংক্রান্ত সমস্যা, গতির সমস্যা নয়। প্রথমে parallel সেটিং পরীক্ষা করুন। মডেলটি সঠিকভাবে কাজ করছে, কিন্তু একবারে একটি অনুরোধ প্রসেস করছে।
Ollama থেকে HTTP 503 ত্রুটি। কিউ পূর্ণ হয়ে গেছে। হয় সার্ভারটি সত্যিই তার সক্ষমতার সর্বোচ্চ পর্যায়ে আছে, অথবা লোড কমানোর জন্য OLLAMA_MAX_QUEUE ইচ্ছাকৃতভাবে কম রাখা হয়েছে, যা আপনি আসলে চাইছেন।
CPU বক্সে লোডের সময় Tokens per second কমে যাওয়া। এমনটি ঘটার সময় vmstat 1 চালান। si এবং so কলামে শূন্য ছাড়া অন্য কোনো মান থাকা মানে মেশিনটি swapping করছে, যার ফলে প্রতিটি টোকেনের জন্য ডিস্ক থেকে ওয়েট (weights) পড়তে হচ্ছে। কোনো কনফিগারেশন পরিবর্তন এটি ঠিক করতে পারবে না। মডেলের আকার বা স্লট সংখ্যা কমিয়ে দিন।
দশজনের মধ্যে একজনের অপেক্ষা অন্যদের চেয়ে অনেক বেশি। vLLM লগে preempted অনুসন্ধান করুন। এর সাধারণ কারণ হলো Preemption এবং এর পুনরায় গণনা (recompute), যার অর্থ হলো আপনার অনুমোদিত কনটেক্সট দৈর্ঘ্যের তুলনায় ক্যাশে (cache) অতিরিক্ত ব্যবহৃত হচ্ছে।
সার্ভার অলস থাকা সত্ত্বেও TTFT খারাপ। এটি কনকারেন্সি নয়, বরং প্রিফিল (prefill) সংক্রান্ত সমস্যা। দীর্ঘ প্রম্পটের ক্ষেত্রে প্রথম টোকেন আসার আগে প্রকৃত সময় ব্যয় হয়, তাই হার্ডওয়্যার দেখার আগে প্রম্পটের আকার এবং প্রিফিক্স ক্যাশিং (prefix caching) পরীক্ষা করুন। যদি দীর্ঘ অপেক্ষা কেবল দীর্ঘ বিরতির পর প্রথম ব্যবহারকারীর ক্ষেত্রেই ঘটে এবং পরবর্তী সবার জন্য ঠিক থাকে, তবে এটি প্রিফিল নয়, বরং Ollama মডেলটিকে আনলোড করে পুনরায় ডিস্ক থেকে ওয়েট পড়ছে। এটি নিশ্চিত করতে অনুরোধের মাঝে মডেলটিকে মেমরিতে রাখা কার্যকর হতে পারে।
FAQ
কেন দ্বিতীয় একজন ব্যবহারকারী ব্যবহার করলে আমার self-hosted LLM ধীর হয়ে যায়?
বেশিরভাগ ক্ষেত্রে এটি ধীর হয় না, বরং সারিবদ্ধ (queue) থাকে। Ollama-তে OLLAMA_NUM_PARALLEL ডিফল্টভাবে 1 থাকে, তাই দ্বিতীয় অনুরোধটি প্রথমটির শেষ টোকেন আসা পর্যন্ত অপেক্ষা করে। অন্য একজন ব্যবহারকারী যখন অপেক্ষা করছেন, তখন প্রথম ব্যবহারকারীর স্ট্রিমটি পরিমাপ করে এই দুটি পরিস্থিতির পার্থক্য বুঝুন: যদি শুরু করার পর তাদের টোকেন প্রতি সেকেন্ডের গতি স্বাভাবিক থাকে, তবে বুঝতে হবে এটি একটি কিউ (queue) সমস্যা এবং parallel count বাড়ালে তা ঠিক হয়ে যাবে। যদি উভয় স্ট্রিমই অর্ধেক গতিতে চলে, তবে আপনি প্রকৃতপক্ষে মেমরি ব্যান্ডউইথ ভাগ করছেন, যা একটি হার্ডওয়্যার সীমাবদ্ধতা।
একটি ছোট GPU কতজন ব্যবহারকারীকে একসাথে সেবা দিতে পারে?
ব্যবহারকারী নয়, মেমরি গণনা করুন। প্রথমে মডেলের ওজন (weights), তারপর KV cache। এর খরচ হলো 2 গুণ লেয়ারের সংখ্যা গুণ key/value হেডের সংখ্যা গুণ হেড ডাইমেনশন গুণ বাইট, প্রতি টোকেন, প্রতি সক্রিয় কথোপকথনের জন্য। 36টি লেয়ার, 8টি key/value হেড এবং 128 হেড ডাইমেনশন বিশিষ্ট একটি সাধারণ 8B মডেলের জন্য 16-বিট মোডে প্রতি টোকেনে প্রায় 144 KiB খরচ হয়। সুতরাং, 8,192 টোকেনের একটি কথোপকথনের জন্য প্রায় 1.2 GB প্রয়োজন। একটি 24 GB কার্ডে ওই মডেলটি 16-বিট মোডে রাখলে ক্যাশের জন্য প্রায় 6 GB অবশিষ্ট থাকে, যা পূর্ণ কনটেক্সটে প্রায় পাঁচটি কথোপকথন চালানোর জন্য যথেষ্ট, অথবা কনটেক্সট ছোট করলে আরও বেশি চালানো সম্ভব।
continuous batching কি প্রতিটি ব্যবহারকারীর উত্তর ধীর করে দেয়?
সাধারণত মিডিয়ান ল্যাটেন্সি উন্নত হয়, কারণ অনুরোধগুলোকে পুরো ব্যাচ শেষ হওয়ার জন্য অপেক্ষা করতে হয় না। তবে টেইল ল্যাটেন্সি (tail latency) খারাপ হয়। প্রতিটি অতিরিক্ত সিকোয়েন্স প্রতিটি ডিকোডিং ধাপে কাজের চাপ বাড়ায়, নতুন আসা অনুরোধের প্রিফিল (prefill) স্ট্রিম করা ব্যবহারকারীদের থেকে কিছুটা সময় কেড়ে নেয় এবং একটি preempted অনুরোধকে দুবার প্রিফিল করতে হয়। গড় ল্যাটেন্সি না মেপে p95 ইন্টার-টোকেন ল্যাটেন্সি পরিমাপ করুন, কারণ চ্যাট উইন্ডোতে বিরতিগুলো এমনভাবে স্পষ্ট হয় যা গড় হিসেবে ধরা পড়ে না।
আমার কি OLLAMA_NUM_PARALLEL বাড়ানো উচিত নাকি vLLM-এ চলে যাওয়া উচিত?
প্রথমে parallel count বাড়ান। এটি বিনামূল্যে করা যায় এবং একটি ড্রপ-ইন ফাইলের মাধ্যমেই সম্ভব। এটি সেই সাধারণ সমস্যাটি সমাধান করে যেখানে চারজন ব্যবহারকারী একটি দীর্ঘ উত্তরের জন্য লাইনে অপেক্ষা করেন। মেমরিই এখানে মূল সীমাবদ্ধতা: সমান্তরাল অনুরোধগুলো আপনার ধরে রাখা কনটেক্সটকে বহুগুণ বাড়িয়ে দেয়, তাই খেয়াল রাখুন যেন লেয়ারগুলো CPU-তে চলে না যায়। যখন আপনার GPU-তে পর্যাপ্ত VRAM থাকে এবং চারটির বেশি অনুরোধ একসাথে চলমান থাকে, তখন vLLM-এ চলে যান। কারণ সেই পর্যায়ে paged cache এবং প্রতি-টোকেন শিডিউলিং তাদের খরচের চেয়ে বেশি সুবিধা প্রদান করে।
আরও বেশি CPU কোর কি ধীর LLM সার্ভার ঠিক করতে পারবে?
ব্যবহারকারীরা যে অংশটি সবচেয়ে বেশি অনুভব করেন, তার জন্য এটি কাজ করবে না। ডিকোডিংয়ের সময় প্রতিটি টোকেনের জন্য মেমরি থেকে পুরো মডেলটি পড়তে হয়, তাই এটি RAM ব্যান্ডউইথ দ্বারা সীমাবদ্ধ। ব্যান্ডউইথ পূর্ণ হয়ে গেলে অতিরিক্ত কোর আর কোনো সাহায্য করে না। প্রিফিল (prefill) অবশ্য কোরের সাথে স্কেল করে, তাই বেশি কোর থাকলে দীর্ঘ প্রম্পটের ক্ষেত্রে প্রথম টোকেন আসার সময় কমে। 4 থেকে 8 GB RAM-এর VPS-এ সাধারণত মেমরি ধারণক্ষমতাই মূল সীমাবদ্ধতা। এক্ষেত্রে আরও vCPU যোগ করার চেয়ে ছোট মডেল ব্যবহার করা বা কনটেক্সট ছোট করা বেশি কার্যকর সমাধান।