你讓一張 GPU 同時替幾個 Agent 整理文件,總 Token/秒看起來變高,長文件卻開始塞不下。這篇 Continuous Batching 教學帶你先分清兩個旋鈕:同時接幾件工作,以及每件工作能保留多少上下文。開更多窗口之前,先算每位客人的桌面還夠不夠。
前半專為完全沒有技術背景的讀者寫,用餐廳解釋排程與記憶體;後半給已有本機模型、願意貼指令的新手。你會學會固定 llama.cpp 版本,比較 1/2/4 槽,留下吞吐、首字等待、Context 與正確交件的紀錄。
以下依 2026 年 11 月 9 日查閱的官方文件與原始碼整理,固定提交碼 7d7f0287530b。本次環境未執行 GPU 推論,本文提供配置與量測方法,效能欄位保留給你的環境填寫。
先說結論:多接單,還要留得住上下文
可用並行數=Context 放得下、等待可接受、交件正確,三關一起過。Continuous batching(連續批次)像餐廳隨時補上空桌:短單完成,就讓下一張單加入後續運算,不必等同批長單全部做完。它改善接單方式;每個 Agent 所需的資訊,仍要放進自己的請求。
第一次先做單槽,再只增加槽數。固定總 KV 容量時,分割配置會縮小每槽上限;unified KV 則讓序列使用共同容量池,仍受總池、模型與每槽限制約束。不要把「單槽可用上限」乘上槽數,就當成全部槽同時可用的容量。
一、Continuous Batching 是什麼?先認清五個零件
① 「正在辦的單」Request:一次送給模型的請求。Agent 可能先問模型、等工具、再問一次;一個 Agent 任務可以有多次請求。Agent Harness 入門把這個模型外的執行迴圈說得更完整。
② 「窗口」Slot:服務能同時處理的序列位置。③ 「桌面」Context:一次推論可使用的 Token 範圍,輸入與後續生成一起占用。Token 是模型處理文字的小單位,不能直接當中文字數。
④ 「已讀筆記」KV Cache:注意力計算留下的中間資料,用來重用先前計算。⑤ 「每輪換桌名單」Continuous batching:在生成的迭代之間調整參與請求。Orca 原始研究提出以迭代為粒度的排程;NVIDIA 的迭代序列文件也說明空位可持續重用。這裡借用機制,不引用它們的效能數字。

prefill 是先讀提示,decode 是逐步生成。長輸入的讀取也會占資源;「已經開始串流」和「新請求很快見到第一字」需要分別量。Anyscale 的推論入門可補上兩階段與記憶體構成。白話說,餐廳一直有客人,不代表每位客人都更快吃到飯。
二、Continuous Batching 教學的容量關鍵:分割 KV 與 unified KV
先把顯存分成權重、KV 與運算暫存三筆帳。權重像共用食譜,KV 像每桌點餐過程留下的筆記;同一模型服務可讓多個序列參與推論,但筆記與暫存仍要占空間。模型權重量化與 KV 資料型別是不同旋鈕,先固定它們,才看得出槽數的影響。
本文固定 -c 16384 作為容量練習,並選已有、相容的純文字標準 Transformer GGUF;模型訓練 Context 至少要涵蓋此設定。這個 Token 數是測試配置,與顯卡有幾 GB 並非一比一換算。循環、混合架構與滑動視窗模型可能有其他狀態配置,第一輪先避開,之後依同版本日誌另驗。
在此提交的 Context 初始化程式中,關閉 unified KV 時,序列容量由總容量除以最大序列數,再作對齊。16384 搭配 1/2/4 槽,算術上分別是 16384/8192/4096;這組數字可整除且符合該處對齊,仍須讀實際日誌。

打開 --kv-unified 時,同一處程式把序列容量設為總池容量;server 的 n_ctx_slot()再套用模型訓練 Context 與選用的每槽上限。單一長單可能使用較多池空間,但多張長單競爭的仍是同一池;配置上限不等於同時占滿的保證。
這版另有 --kv-unified-per-slot N。若不寫 -c,啟動參數處理會按槽數乘 N 設池;若已固定 -c 16384,就保留這個總池設定,再套每槽限制。想比較「相同總容量」時必須明寫 -c,否則可能連總容量一起改了。
還有一個容易踩的版本差異:這版 -np 預設為 auto,server 在 auto 路徑選四槽並啟用 unified KV。因此「沒寫 -np」與「明寫 -np 4」不宜當成同一配置。實驗每次都明寫槽數與 KV 模式,並保存 n_ctx、n_ctx_seq、n_ctx_slot、n_slots 和 kv_unified。
三、五步上手:固定版本,再比較 1/2/4 槽
①「先認廚房」:保存執行器與模型身分
已有 Git、CMake、編譯器與相容 GPU 後端的人,在新的練習目錄取固定版原始碼;已有正確版本的執行檔則可直接使用。依 官方建置指南選自己的後端;下列是已備妥 CUDA 工具鏈的 NVIDIA 範例,其他平台改用該文件的 Metal、HIP 或 Vulkan 路徑。
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
git checkout 7d7f0287530bc8981690f361d8c1627dbae1c99a
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release --target llama-server -j 2
./build/bin/llama-server --version
記錄完整 commit、編譯選項、GPU/驅動、GGUF 檔名與雜湊;Linux 可用 sha256sum model.gguf,macOS 用 shasum -a 256 model.gguf。模型沿用自己已取得且有權使用的檔案;這輪不換量化、不開草稿模型。雜湊用來確認成品相同,能力仍由交件驗收。
②「先開一桌」:讀懂啟動日誌
將 model.gguf 換成你的實際路徑,先啟動單槽。以下限定一張裝置,僅監聽本機;--fit off 關閉自動調整未設參數,讓配置更容易對帳。若全層 GPU 放不下,記錄啟動失敗,另選較小模型或固定較少 GPU 層數,三檔都用同一配方。
./build/bin/llama-server -m /absolute/path/model.gguf --alias local --host 127.0.0.1 --port 8080 --split-mode none --main-gpu 0 -ngl all --fit off -c 16384 -np 1 --no-kv-unified --cont-batching --no-context-shift -b 512 -ub 128 -ctk f16 -ctv f16 --metrics --slots --log-file split-p1.log
另開終端機執行 curl --fail-with-body http://127.0.0.1:8080/health,成功後保存 /props 與 /slots 的 JSON。固定版參數定義可逐項核對旗標;槽位 API 和 metrics 只在這個本機練習開啟,不把監聽改成對外位址。
③「同一份訂單」:準備十二份假文件
建立 docs 資料夾,放入十二個 .txt,各有不同 id、owner、deadline。先用很短的文件確認管線,例如 id=DEMO-01; owner=Lin; deadline=2026-12-01;其餘以 DEMO-02 到 DEMO-12 編號,自己先寫答案本。每份要求回一個 JSON,三欄完全相符才合格。
再另存一份長版題組:在不改三個答案的情況下,加上自寫的無關段落。先透過 /apply-template 與 /tokenize 估完整提示長度,保留輸出與模板的空間;執行後再核對回應的 Token 計數。短、長題組分開跑,不把相同字數當成相同 Token 數。十二題只是入門練習,尾端延遲仍要更多樣本。
④「放上碼表」:保存每個請求的首字與結果
將下面存為 bench.py,在含 docs 的目錄執行。使用 Python 標準函式庫,依 該版串流回應實作讀取內容、usage 與 timings;首字定義為第一段非空白可見文字,不把 reasoning 欄位當成答案首字。錯誤與空值會留下,不自動重試或改答案。
import concurrent.futures as cf
import json
import pathlib
import sys
import time
import urllib.error
import urllib.request
workers, label = int(sys.argv[1]), sys.argv[2]
assert workers in (1, 2, 4)
out = pathlib.Path(label)
out.mkdir(exist_ok=False)
docs = sorted(pathlib.Path('docs').glob('*.txt'))
assert len(docs) == 12, 'docs must contain 12 text files'
def run(path):
start = time.perf_counter()
first = None
answer = ''
usage = {}
timings = {}
body = {
'model': 'local', 'stream': True,
'stream_options': {'include_usage': True},
'messages': [
{'role': 'system', 'content': 'Extract id, owner, deadline. Return one JSON object only.'},
{'role': 'user', 'content': path.read_text(encoding='utf-8')}
],
'temperature': 0, 'seed': 42, 'max_tokens': 256,
'cache_prompt': False
}
req = urllib.request.Request(
'http://127.0.0.1:8080/v1/chat/completions',
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'})
error = None
try:
with urllib.request.urlopen(req, timeout=300) as response:
for raw in response:
if not raw.startswith(b'data:'):
continue
data = raw[5:].strip()
if data == b'[DONE]':
break
chunk = json.loads(data)
if chunk.get('error'):
raise RuntimeError(json.dumps(chunk['error']))
if chunk.get('usage'):
usage = chunk['usage']
if chunk.get('timings'):
timings = chunk['timings']
for choice in chunk.get('choices', []):
piece = choice.get('delta', {}).get('content') or ''
if piece.strip() and first is None:
first = time.perf_counter() - start
answer += piece
if usage.get('completion_tokens') is None:
raise ValueError('Missing final token usage')
except Exception as exc:
error = type(exc).__name__ + ': ' + str(exc)
return {'file': path.name, 'ttft_s': first,
'elapsed_s': time.perf_counter() - start,
'answer': answer, 'usage': usage, 'timings': timings, 'error': error}
start = time.perf_counter()
with cf.ThreadPoolExecutor(max_workers=workers) as pool:
rows = list(pool.map(run, docs))
elapsed = time.perf_counter() - start
(out / 'requests.json').write_text(json.dumps(rows, ensure_ascii=False, indent=2))
counts = [r['usage'].get('completion_tokens') for r in rows]
complete = all(n is not None and r['error'] is None for n, r in zip(counts, rows))
summary = {'workers': workers, 'wall_s': elapsed,
'output_tokens_per_s': sum(counts) / elapsed if complete else None,
'errors': sum(r['error'] is not None for r in rows)}
(out / 'summary.json').write_text(json.dumps(summary, indent=2))
print(json.dumps(summary))
此腳本是小型量測器,尚未加入生產服務的取消控制、重試政策與多租戶隔離;請求超時會保存錯誤,測試後確認服務端是否仍有工作。它以固定在途數維持負載,elapsed_s 從送出請求開始,不包含等候 Python 工作窗口的時間;整批 wall_s 則包含十二件工作的完整排程。
python3 bench.py 1 split-p1-r1
停止原服務,確認程序結束後,將啟動指令的 -np 改成 2、日誌名改成 split-p2.log;啟動並確認 health,再執行 python3 bench.py 2 split-p2-r1。四槽同樣改成 4。服務槽數與用戶端在途數都要變,否則只開窗口,沒有同時送單。
⑤「再換共用桌」:只改 KV 模式
完成三檔分割配置後,將 --no-kv-unified 換成 --kv-unified,仍保留 -c 16384,再走 1/2/4 槽。先不設每槽 cap 觀察日誌;要試 --kv-unified-per-slot 4096,另開一輪並標名,避免把限制不同的結果混在一起。
每個配置先送一個不列入計分的暖機請求,再跑完整題組;每輪重啟服務,短題組與長題組各先做三輪,輪替配置順序。腳本明寫 cache_prompt: false,第一輪先排除請求前綴重用;要研究共同提示詞,再另設 true 與不同前綴的對照,不把暖快取收益歸給並行槽。
四、怎麼填工作表?吞吐、等待與顯存各回答一題
總輸出吞吐=十二個請求的 completion_tokens 總和 ÷ 整批 wall_s。欄位缺失或出錯時,腳本留空吞吐;先處理失敗,不能把部分完成當完整批量。timings.predicted_per_second 是服務端生成統計,completion_tokens / elapsed_s 則是請求端含等待的粗略速率,兩個名稱分開寫。
工作表每列填:版本/模型雜湊、KV 模式、總池、槽數、實際每槽上限、在途數、題組、整批秒數、總輸出 Token/秒、逐件 TTFT、逐件完成秒數、錯誤數、合格件數、峰值記憶體。取三輪中位數並保留每輪原值;十二題的慢端只能當線索,先別把它包裝成穩定的生產 P95。
依 NVIDIA-SMI 官方參數,NVIDIA 練習可在獨立終端機先開下面的取樣,再開始量測,完成後按 Ctrl-C 停止。它記整張卡的裝置占用,包含其他程序,0.2 秒取樣也可能漏掉瞬間尖峰;關閉無關 GPU 工作,標成「取樣峰值」而非精確程序峰值。AMD 與 Apple Silicon 改用各平台工具,記憶體口徑各自標示。
nvidia-smi --query-gpu=timestamp,index,memory.used --format=csv --loop-ms=200 > gpu-memory.csv
答案也要開檔核對:JSON 能解析、三欄正確、沒有漏題或額外編造,才算合格。若總吞吐升高但長文件失敗,先減少在途數或調整容量;若容量足夠但首字等太久,檢查長提示與排隊。採用門檻由你的工作決定,例如每件須在十秒內見到可見文字;這是自訂門檻,不是本文量得的速度。
五、再加三個反例:長短混跑、超長輸入、相依任務
長短混跑:先讓長單進入生成,再送短單,保存兩者時間線與槽位狀態。用同題、同槽、同 KV 模式,把 --cont-batching 換成 --no-cont-batching 做另一輪,才更接近檢查排程機制;只比 1/2/4 槽,是整體配置比較,並未隔離連續批次的獨立效果。
超長輸入:在隔離練習增加一份超過日誌每槽上限的文件,保存 HTTP 回應與服務日誌,成功時也檢查是否被截斷;這輪不與十二題吞吐混算。本文明寫 --no-context-shift,先避免把丟棄舊內容後繼續生成當成完整保留。具體拒絕或截斷行為按端點與輸入設定核對。
相依任務:A 先抽取日期,B 才能依 A 的結果排序;B 等 A 交件後再送。可並行的是不同文件的抽取,彙總放在後面。每個任務只寫自己的輸出檔,以 task_id 串接,別讓四個 Agent 同時改同一份成品。這是 Harness 的資料流設計,KV 共池不會替你完成交接。
共同 system prompt 可以設計前綴重用測試;「背景內容意思相似」和「Token 前綴可重用」是兩件事。想往跨機器擴展,再看 跨 Worker KV Cache 擴充;這一篇先把單機容量與每次請求量清楚。
常見問題:八個直接答案
1. 四槽就是四個完整 Agent 嗎?
不是同一層。槽位接模型請求;工具、檔案、狀態與任務相依由 Harness 管,一個 Agent 也可能多次借用槽位。
2. 總 Token/秒變高,我的單次回答一定更快嗎?
不一定。總吞吐看整批工作,單次體驗看 TTFT 與完成時間;兩張成績單都留下。
3. -c 16384、四槽,每槽一定 4096 嗎?
分割 KV 的這組算例是 4096;unified 模式使用另一套容量邏輯,還要看模型與每槽 cap,直接核對 n_ctx_slot。
4. unified KV 讓四槽各自同時用滿 16384 嗎?
不能這樣推算。共享的是同一總池;各序列上限與全部序列同時占用量要分開看。
5. –kv-unified-per-slot 會自動擴大固定的 -c 嗎?
本文固定版保留已明寫的 -c;未寫 -c 時才走按槽數乘上限設池的路徑。保存最後日誌確認。
6. 設 -b 512,就是開 512 個窗口嗎?
不是。-b 是邏輯批次 Token 容量,-ub 是實體 microbatch 上限,-np 才是 server 槽數;它們控制不同事情。
7. 關掉 cache_prompt 就能保證每次文字完全相同嗎?
不保證。固定 seed 與採樣仍要保存輸出;批次和後端可能改變數值路徑,品質逐件核對。
8. 今天該選一、二還是四槽?
先選三關都過的配置。只有一件相依長任務,先一槽;多份獨立短文件,測二槽再四槽。硬體未備妥就先完成答案本與工作表。
給新手的三個重點
先固定配方:模型、量化、版本與總池不動,只換一個旋鈕。再讀真實上限:槽數、每槽 Context 與共池容量分欄。最後驗交件:吞吐變高,也要每件在可接受時間內正確完成。
接著閱讀
左右滑動查看更多推薦
下一步:先交出一張能對帳的容量單
今天先記下版本與模型雜湊,跑通一槽、十二份短文件,核對十二個答案。再開二槽,觀察每槽上限與等待怎麼變。記住:Context 放得下、等待可接受、交件正確,才是可用並行。想循序把模型接成工作系統,可從 AlphaLab AI 專區繼續選題,或到 AlphaLab 課程安排下一段練習。






