前兩天把模型跟資料結構準備好,今天來處理另一個後面會一直遇到的東西:async / await。
如果去看 MCP Python SDK,很快就會看到大量:
async def
await
async with
所以 async 基本上躲不掉。
但今天不打算花太多篇幅講語法,我比較想知道一件事:
很多請求同時打進本機模型,真的會變快嗎?
我覺得理解 async 最簡單的一句話就是:
async 解決的是等待時間,不是計算時間。
例如 Agent 很常在等:
LLM 回傳結果
API Response
工具執行完成
另一個 Agent 回報
同步程式會停在原地等。
async 則可以在等待時,先去處理其他工作。
例如原本八個問題一題一題問:
for prompt in PROMPTS:
await one(llm, prompt)
改成:
results = await asyncio.gather(
*(one(llm, prompt) for prompt in PROMPTS)
)
八個請求就可以一起送出去。
這次用 llama3.2:3b 測試。
序列 5.16s 299 tok 57.9 tok/s
並行 3.38s 283 tok 83.7 tok/s
加速 1.53x
確實有變快。
但八個請求一起跑,並沒有快八倍。
只有大約 1.5 倍。
原因也很直接:
async 可以讓請求一起送出去,但 GPU 還是只有一張。
所以我乾脆把並行度一路往上拉。
實測結果:
| 並行度 | 總耗時 | 吞吐量 |
|---|---|---|
| 1 | 1.53s | 73.7 tok/s |
| 2 | 2.73s | 82.7 tok/s |
| 4 | 5.23s | 86.4 tok/s |
| 8 | 10.33s | 87.5 tok/s |
| 16 | 20.42s | 88.5 tok/s |
這張表就很有意思了。
並行度:
1 → 2 → 4 → 8 → 16
一路翻倍。
但吞吐量:
73.7 → 82.7 → 86.4 → 87.5 → 88.5 tok/s
到後面幾乎不太動了。
反而總耗時還在一直增加。
也就是說:
超過某個並行度之後,請求並沒有真的一起變快,而是在排隊。
這就是單張 GPU 的物理限制。:contentReference[oaicite:0]{index=0}
後面做到 Multi-Agent 時,可能會長這樣:
Supervisor
/ | \
Agent A Agent B Agent C
看起來三個 Agent 同時工作,理論上應該很快。
但如果最後都是:
Agent A ─┐
Agent B ─┼─→ 同一張 GPU
Agent C ─┘
那 Agent 數量增加,不代表算力也跟著增加。
這也是為什麼今天我要先把這張表量出來。
等到 Day 29 接 NVIDIA PAIR 時,我們會再回來看這個問題。
還有一件事一定要記:
不要在 async 裡面直接跑同步阻塞程式。
例如:
async def run():
time.sleep(5)
time.sleep() 會直接把整個 Event Loop 卡住。
如果真的有同步函式,可以改成:
await asyncio.to_thread(blocking_work)
常見的同步阻塞來源像是:
time.sleep()
requests.get()
subprocess.run()
同步檔案 I/O
這種問題最麻煩的地方是:
程式通常不會報錯,只會莫名其妙變慢。
今天我自己會記三件事:
1. async 省的是「等」的時間,不是「算」的時間
2. 並行度增加,不代表 GPU 吞吐量會一直增加
3. Multi-Agent 開更多 Agent,也可能只是一起排隊
今天量到這張卡大約在:
87 ~ 88 tok/s
附近開始趨近飽和。
先把這個數字記著。
等後面做到 Multi-Agent,再回頭看會更有感。
下一篇:
裝飾器、Context Manager、Generator,手刻一個 @tool 出來。
Day 2 已經看到:
Pydantic
↓
JSON Schema
明天再往前一步,自己做:
@tool
def read_file(...):
...
看看一個普通 Python function,是怎麼一步一步變成 Tool 的。