自架 LLM 使用者一多就變慢的原因與解決方案
自架 LLM 在 5 位使用者同時連線時卡住,主因在於預設的平行處理限制與排隊機制。本文分析 Prefill 與 Decode 階段的記憶體頻寬瓶頸,並說明如何透過 Batching 與 KV Cache 優化,提升伺服器同時服務多位使用者的吞吐量。
為什麼自架 LLM 在使用者增加時會變慢?
自架 LLM 在 5 個使用者同時連線時會卡住,是因為伺服器一次僅能產生一個回覆,其餘四個請求則處於佇列中。Ollama 的說明文件對預設值描述得很直接:OLLAMA_NUM_PARALLEL 是「每個模型同時處理的最大平行請求數,預設值為 1」。這並非故障,而是您的五位使用者中有四位正在排隊等待。
解決此問題通常不需要更換效能更強的硬體。您需要的是一個能將多個請求合併在同一次前向傳遞(forward pass)中進行處理的服務引擎,並確保有足夠的閒置記憶體來儲存所有使用者的對話內容。這兩者缺一不可,而後者才是真正決定您效能上限的關鍵。
每個請求必須經歷的兩個階段
Prefill 會一次讀取整個提示詞並建立注意力快取(attention cache)。由於所有提示詞 token 會同時通過模型,因此 prefill 是一次大型矩陣乘法,其效能受限於算術處理能力。隨後的 Decode 階段則會逐一輸出 token。每個 token 都需要將模型的完整權重重新從記憶體讀出,但針對該單一 token 進行的算術運算量極小。因此,Decode 階段受限於記憶體頻寬。
這種不對稱性正是批次處理(batching)有效的核心原因。若僅為單一使用者進行解碼,每個 token 需讀取約 5 GB 的權重,導致大部分算術運算單元處於閒置狀態。若加入第二個請求,引擎只需讀取相同的 5 GB 一次,即可同時計算出兩個 token。第二位使用者的額外耗時幾乎為零。若嚴格採取逐一處理請求的方式,將會浪費此優勢。
使用者感受取決於兩個指標。TTFT (time to first token) 為排隊等待時間加上 prefill 時間。ITL (inter-token latency) 為串流 token 之間的間隔,由 decode 階段決定。伺服器回應緩慢通常源於其中之一,且兩者的解決方案並不相同。在調整任何設定前,務必先釐清問題所在,而 分別測量 prefill 與 decode 的時間 即為判斷方法。
靜態批次處理會導致所有請求受限於最慢的回應
靜態批次處理(Static batching)屬於較原始的實作方式,若您在應用程式碼中自行分組請求,通常會遇到此問題。引擎會收集 N 個請求並同時執行,且必須保留所有插槽(slot),直到該批次中最長的生成任務完成為止。
當某位使用者請求 1,200 個 token 的摘要時,會導致另外四個單行回答被鎖定在該批次中,因為在最慢的任務完成前,批次不會釋放任何插槽。
這會產生兩項成本。已完成的序列會持續佔用無法進行有效運算的插槽,導致輸出長度不一時,整體有效吞吐量下降,而聊天輸出的長度通常差異極大。若某個請求在批次形成後才送達,則必須等待整個批次執行完畢才能開始預填充(prefill),這意味著該請求的首字延遲(TTFT)完全取決於他人請求的長度。
連續批次處理會在每個 token 產生後准入並移除請求
連續批次處理(Continuous batching)以單一解碼步驟為排程單位。在每個步驟完成後,排程器會移除剛輸出停止 token 的序列,並將等待中的請求納入空出的槽位。若某個回應在第 40 步結束,該槽位即於第 40 步釋放,而非等待整批處理結束。
這並非罕見技術。llama-server 將 -cb, --cont-batching 記錄為「是否啟用連續批次處理(又稱動態批次處理)(預設:啟用)」,而 vLLM 的架構核心即是此概念。Ollama 同樣支援平行請求。預設值僅將數量限制為 1,這也是為何許多人誤以為其硬體無法處理並發,實際上卻是設定檔限制了功能。
已發表的連續批次處理效能數據,通常是在具備閒置運算資源與數十 GB 快取空間的資料中心等級卡片上測得。這些數據的效能趨勢適用於您的設備,但規模則不然,原因請見下方的記憶體章節。
Prefill 與 decode 競爭相同的運算資源
當四個回覆正在串流傳輸時,若有新請求進入,其 prompt 必須先進行 prefill,而 prefill 非常消耗運算資源。如果排程器為該 prefill 分配獨立的步驟,則這四個正在串流的使用者將會在該期間無法收到任何 token。對於長 prompt 而言,這會導致每個開啟的視窗出現明顯停頓。這就是使用者所說的「當有人發送請求時,伺服器就會卡頓」的現象。
Chunked prefill 將長 prompt 分割成多個片段,並將每個片段與正在進行的 decode 混合在同一個步驟中執行。vLLM 的調校指南直接指出了其中的取捨:較小的 chunk budget 「能獲得更好的 ITL,因為減少了拖慢 decode 速度的 prefill 次數」,而較高的數值則「能獲得更好的 TTFT,因為可以在一個批次中處理更多的 prefill token」。您必須決定要優先保障哪一方的體驗:是等待回覆開始的人,還是正在觀看文字串流的人。
Prompt 長度決定了這種影響的嚴重程度。一個 6,000 個 token 的 prompt 搭配 200 個 token 的回答,意味著 6,000 個 token 的 prefill 工作量對比 200 個 decode 步驟。檢索增強生成 (RAG) 對話與長系統提示詞都會將您推向這種情況,此時 prefill 不再是可忽略的誤差,而成為使用者必須等待的瓶頸。當長的部分重複出現時,Prefix caching 有所幫助:vLLM 提供了 --enable-prefix-caching,它能重複使用共享 prompt 前綴的快取,而無需為每個請求重新計算。
最先耗盡的記憶體是 KV cache
活躍對話中的每個 token 都會在模型的每一層留下一個 key vector 與一個 value vector。這就是 KV cache (key/value cache),它讓解碼過程無須為每個新 token 重算整個 prompt。每個 token 的快取大小由模型架構決定:2 (一個 key,一個 value) 乘以層數,乘以 key/value head 數量,乘以 head 維度,再乘以每個數值的位元組數。請從模型的 config.json 讀取這些數值。
計算一次後,上限就不再是謎團。一個典型的 8B 模型擁有 36 層、8 個 key/value head 與 128 的 head 維度,若以 16-bit 儲存快取,每個 token 的成本為 2 36 8 128 2 位元組。這等於 147,456 位元組,約 144 KiB。因此,一場 8,192 token 的對話大約需要 1.2 GB 的快取。五場對話則需要約 6 GB,這還是在模型權重之外的額外需求,這才是能容納多少使用者的真正答案。
並發會倍增上下文需求,工具說明也明確指出這點。Ollama 的 FAQ:「針對特定模型進行平行請求處理,會導致上下文大小隨平行請求數量增加。例如,2K 上下文搭配 4 個平行請求,將導致 8K 上下文與額外的記憶體配置。」所需的 RAM 會隨 OLLAMA_NUM_PARALLEL 乘以 OLLAMA_CONTEXT_LENGTH 進行擴展。在 llama-server 中,您透過 -c 設定的上下文會分配給 -np 個插槽 (slots),因此單獨增加插槽數量會壓縮每個請求可容納的空間。請從啟動日誌中讀取每個插槽的上下文大小,不要憑空臆測。
vLLM 則採取預先配置策略。--gpu-memory-utilization (預設為 0.92) 是「模型執行器所使用的 GPU 記憶體比例」。扣除權重後剩餘的空間會成為分頁式 KV 池 (paged KV pool),當該池空間不足時,排程器會選擇驅逐請求而非讓其失敗:
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 引擎中,預設的搶佔模式為 RECOMPUTE,因此被驅逐的請求會丟棄其快取,並在重新進入時再次進行預填充 (prefill)。這項工作會被執行兩次。文件警告「搶佔與重新計算可能會對端對端延遲產生負面影響」,而這行日誌是解釋為何某位倒楣的使用者等待時間遠長於其他人,但您的平均數據看起來卻很正常的最有力證據。請設定 disable_log_stats=False 來記錄累計次數,或從 vLLM 暴露的 Prometheus 指標中讀取搶佔計數器。
當並發使用者數達到 2、5 與 20 時的變化
兩位使用者。 在具備充足快取空間的 GPU 上幾乎無感,因為第二個解碼串流會與第一個並行,僅增加極少的額外時間。但在僅有 4 到 8 GB RAM 的純 CPU VPS 上,這並非免費:兩個串流會共用相同的 vCPU 與 RAM 頻寬,因此每位使用者看到的每秒 Token 數(tokens per second)大約減半,且快取需求在極其有限的預算下翻倍。
五位使用者。 這是預設值不再足夠的臨界點,問題始於佇列。當 OLLAMA_NUM_PARALLEL 設為 1 時,四個人必須等待正在接收長回覆的使用者,一旦輪到他們,每個人都能獲得正常的處理速度。若提高並行數(parallel count),問題的性質就會改變:五個插槽各佔用 8K 上下文,意味著需要 40K 的 Token 快取空間。如果 VRAM 放不下,引擎會將層級卸載(offload)至系統 RAM;若 RAM 也放不下,系統就會開始 Swap,導致每秒 Token 數崩潰。
二十位使用者。 在聊天介面中,二十位使用者通常不代表二十個並發請求,這是購買硬體前最需要理解的事。使用者在回覆間隔通常會閱讀並思考 20 到 60 秒,因此大部分時間處於閒置狀態。但二十個代理程式(agent)或二十個文件摘要任務,則是二十個完全沒有閒置時間的真實串流。這需要完全不同等級的機器。若開發者將 編碼代理程式指向自己的 Ollama 伺服器,其情況會更接近後者,因為代理程式會在任務執行期間持續發送請求,不會像人類一樣有閱讀停頓。
您的使用者是同時在線,還是僅為登入狀態?
在進行任何規模評估前,請先計算傳輸中的請求數量。計算方式很簡單:傳輸中的請求數等於使用者人數,乘以每回合產生的秒數,再除以回合間隔秒數。
- 首先測量您單一串流的速度,包含預填充(prefill)與解碼(decode)。請勿直接引用他人的數據:在您的機器上測量每秒 Token 數 並使用實際測得的數值。
- 估算工作週期(duty cycle)。若有 20 位聊天使用者,每回合產生 12 秒,每 90 秒進行一回合,則為 20 * 12 / 90,約等於 2.7 個傳輸中的請求。
- 將插槽(slot)數量設定為略高於此數值,接著根據記憶體進行驗證:插槽數乘以每個請求的上下文大小,必須小於您實際擁有的快取 Token 容量。
- 保持佇列(queue)簡短,以便在溢位時能快速且明顯地失敗。
可用的快取 Token 為扣除權重後剩餘的可用記憶體,除以上述章節提到的每個 Token 成本。一張 24 GB 的顯示卡執行 16-bit 的 8B 模型時,權重約佔用 16 GB,在預設使用率下約有 6 GB 的可用快取,這大約能容納五個 8K 的對話。若要容納更多,請縮短每個請求的上下文,或將快取儲存為 8-bit(llama-server 使用 --cache-type-k q8_0)。兩者皆透過犧牲部分效能來換取並發能力,在投入硬體預算前,建議先閱讀此權衡的真實情況:GPU VPS 在何種情況下比 API Token 更划算。
Ollama 預設值不足時的處理方式
透過服務單元(service unit)提高並行處理數量,因為 shell 的 export 指令無法影響由 systemd 管理的 daemon。
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 應顯示您剛設定的三個變數。若未顯示,表示 drop-in 設定檔未儲存成功,後續操作將無效。ollama ps 接著會列出已載入的模型,其大小會大於權重本身,因為 4 個槽位(每個 8,192 tokens)會在旁邊預留 32,768 tokens 的快取空間。若 PROCESSOR 欄位顯示部分模型位於 CPU,而您預期應全數在 GPU 上執行,代表您要求的快取空間超過了顯示卡剩餘記憶體。請調降這兩個數值中的其中一個。通常縮減 context 較為安全,但若視窗過小,系統會默默截斷長提示詞(prompt)而非報錯,因此建議 審慎設定 num_ctx,而非僅是為了讓模型塞入而盲目調降。
佇列預設值值得重新檢視。Ollama 最多可佇列 OLLAMA_MAX_QUEUE 個請求,且「預設值為 512」。超過此數值後,伺服器會回應「503 錯誤,表示伺服器負載過重」。在一個同時僅能處理 4 個請求的機器上設定 512 的深度,是一個無法兌現的承諾,因為排在第 300 位的客戶端在輪到它之前就會逾時。較短的佇列能回傳錯誤,讓您的應用程式進行重試或回報,這比永遠不會結束的載入轉圈更有效。
進行實際測試。從兩個終端機同時發送請求並觀察兩者。若第二個請求在第一個完成前完全沒有反應,代表並行設定未生效。
何時該使用正式的推論引擎
當您的 GPU 尚有餘裕且同時處理超過約 4 個請求時,vLLM 帶來的效能提升便能抵銷其設定成本。其排程器以 token 為單位運作,並透過分頁快取(paged cache)重複利用閒置的記憶體碎片,將多餘的 VRAM 轉化為併發處理能力,而非任其閒置。截至 2026 年 8 月,官方文件建議的安裝與啟動指令如下:
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(單次迭代中可處理的最大 token 數)。前者限制併發量,後者則是前述的分塊預填充(chunked prefill)預算。
若同時處理的請求少於 4 個,或是在沒有支援 GPU 的機器上,vLLM 的複雜度將高於其帶來的效益。它要求使用 CUDA 等級的顯示卡,且會在啟動時佔用大部分記憶體,這對於 4 GB 至 8 GB 的 VPS 而言並不划算。在這種情況下,建議使用較小的模型、較短的上下文長度,並自行管理請求佇列。Ollama 與 vLLM 作為推論引擎的差異 詳細說明了選擇標準,而 在 VPS 上執行 Qwen 3 8B 則展示了中型模型在尚未加入任何額外使用者前所需的資源需求。
民間傳說隱藏的權衡
Continuous batching 能提升總吞吐量,通常也能改善中位數延遲,因為排隊的請求能更早開始處理。然而,尾端延遲(tail latency)的變化卻截然不同,且這部分鮮少被提及。
在處理步驟中,每增加一個序列都會產生額外負擔,因此隨著批次填滿,所有使用者的 ITL 都會上升。新進請求的 prefill 會佔用串流使用者原本可享有的處理時間。在快取壓力下,排程器會執行搶佔(preempt),這會導致生成到一半的請求被迫回到 prefill 起點。
聊天介面呈現的是尾端延遲而非平均值。若串流在句子中間停頓兩秒,即使總完成時間尚可,使用者仍會認為服務故障。請在預期負載下測量 p95 TTFT 與 p95 ITL,並將每秒平均 token 數視為容量指標,而非使用者體驗的描述。
實務設定應由此導出。將並發數限制在記憶體允許範圍之下,確保引擎無需執行搶佔。短而可預測的佇列優於會導致抖動(thrash)的深層批次;因為等待四秒後能流暢串流的使用者,滿意度高於立即開始卻中途停頓兩次的使用者。
當效能緩慢時的檢查項目
個別使用者正常,但等待時間過長。 這屬於佇列問題,而非速度問題。請先檢查平行處理設定。模型目前運作正常,但一次僅能處理一個請求。
收到來自 Ollama 的 HTTP 503 錯誤。 代表佇列已滿。伺服器可能已達負載上限,或是 OLLAMA_MAX_QUEUE 被刻意設為較低值以進行負載卸載,這正是預期的行為。
在 CPU 伺服器上負載過高時,每秒生成 Token 數(TPS)崩潰。 請在發生時執行 vmstat 1。若 si 與 so 欄位出現非零數值,代表系統正在進行 Swap,導致每個 Token 都必須從磁碟讀取權重。任何設定調整都無法解決此問題,請縮減模型大小或減少槽位(slot)數量。
十位使用者中有一位等待時間遠長於其他人。 請在 vLLM 日誌中搜尋 preempted。通常原因是搶佔(preemption)與後續的重新計算,這代表快取對於您允許的上下文長度而言已過度分配。
即使伺服器閒置,TTFT(首字延遲)依然很差。 這屬於預填充(prefill)問題,而非併發問題。長提示詞在出現第一個 Token 前需要實際運算時間,因此在檢查硬體前,請先檢視提示詞長度與前綴快取(prefix caching)。若長等待僅發生在一段閒置後的首位使用者,且後續使用者皆正常,這並非預填充問題,而是 Ollama 卸載了模型並重新從磁碟讀取權重。建議透過 在請求間保持模型常駐 來排除此狀況。
FAQ
為什麼我的自架 LLM 在第二個人使用時會變慢?
通常它並未變慢,而是在排隊。Ollama 預設將 OLLAMA_NUM_PARALLEL 設為 1,因此第二個請求必須等待第一個請求輸出最後一個 token 後才能開始。若要區分這兩種情況,請在另一位使用者等待時測量其中一人的串流速度:如果他們開始輸出後 token per second 正常,代表是在排隊,調高並行數即可解決。如果兩個串流的速度都減半,則代表是在共享記憶體頻寬,這是硬體限制。
一張小型 GPU 可以服務多少並行使用者?
請計算記憶體,而非使用者人數。先計算權重,接著是 KV cache。KV cache 的成本為:2 × 層數 × key/value heads × head dimension × 位元組數,這是針對每個 token、每個活躍對話的計算量。一個典型的 8B 模型(36 層、8 個 key/value heads、head dimension 為 128),在 16-bit 下每個 token 約需 144 KiB,因此 8,192 個 token 的對話約需 1.2 GB。一張 24 GB 的顯示卡在載入該模型後,約剩 6 GB 可用於 cache,這大約能支撐 5 個全上下文的對話,若縮短上下文則可容納更多。
連續批次處理(continuous batching)會讓每個使用者的回覆變慢嗎?
中位數延遲通常會改善,因為請求不再需要等待整個批次完成。但尾端延遲(tail latency)會變差。每個額外的序列都會增加解碼步驟的負擔,新進請求的預填充(prefill)會佔用串流使用者的部分步驟,且被搶佔的請求必須進行兩次預填充。請測量 p95 的 token 間延遲(inter-token latency)而非平均值,因為聊天視窗中的停頓在平均值中會被掩蓋,但使用者感受卻很明顯。
我應該調高 OLLAMA_NUM_PARALLEL 還是改用 vLLM?
請先調高並行數。這不需要額外成本,只需一個設定檔即可完成,且能解決四個人排隊等待一個長回覆的常見問題。記憶體是限制因素:並行請求會倍增所需的上下文空間,請留意是否有層數溢出到 CPU。當您擁有充足的 VRAM 且同時有超過四個請求真正執行時,再改用 vLLM,因為此時分頁快取(paged cache)與逐 token 排程帶來的效益才會大於其成本。
增加 CPU 核心數能修復緩慢的 LLM 伺服器嗎?
對於使用者最有感的環節並無幫助。解碼過程在每個 token 產生時都要從記憶體讀取整個模型,因此受限於 RAM 頻寬,一旦頻寬飽和,增加核心數也無濟於事。預填充(prefill)確實會隨核心數擴展,因此更多核心能縮短長提示詞的「首個 token 時間」(time to first token)。在 4 到 8 GB 的 VPS 上,主要的限制通常是記憶體容量,有效的解決方案是使用較小的模型或縮短上下文,而非增加 vCPU。