大模型推理過程中的效能最佳化方法


本文介紹大模型推理效能的最佳化方法。
包括三個方面:服務調度、計算引擎和模型演算法。
本文參考:孫慶虎(故北),公眾號:阿里雲開發者
【萬字長文】大模型訓練推理和性能優化演算法總結和實踐
優化大模型推理性能,就是找一種平衡 —— 既要讓 GPU 算得快、不閒著,又要少佔顯存、少顧帶寬,還得兼顧吞吐量和延遲的矛盾。
服務層從調度入手:Continuous Batching 作為調度員,每生成一個 token 就調整批次,讓早結束的請求騰位置、新請求插隊,免得 GPU 空轉;流式生成邊算邊返回,還能中途打斷,既讓用戶等得短,又省了無效計算;流式生成邊算邊返回,還能中途打斷,既讓用戶等得短,又省了無效計算;流式生成邊算邊返回,還能中途打斷,既讓用戶等得短,又省了無效計算; (n×k),讓模型能 「啃」 長文件還不費太多力。
推理引擎層從效率入手:
KV-Cache 像記筆記,存下歷史計算結果避免重複勞動;PagedAttention 學操作系統分頁,解決顯存碎片問題,讓內存用得更透;
APC 則抓 “重複前綴”,比如 100 個用戶問同一篇財報,它只算一次前綴的緩存,剩下 99次直接復用,省了大半算力;
分佈式並行(TP+PP)“分工合作”,把大模型拆給多卡幹,你算這部分我算那部分,既裝下大模型又提速;
算子融合則把零散的計算步驟捆成一團,少讀寫顯存、少啟動 Kernel,讓數據在 GPU 裡更順。
模型量化層則是 「瘦身術」:
把 32 位浮點數壓成 8 位甚至 4 位整數,用點精度損失換顯存減半、速度翻倍,接著靠 SmoothQuant、GPTQ 這些技巧,讓 “瘦身後” 的模型依然能打。
以下是這三個層面的詳細闡述:
一、服務層優化
(一)Continuous Batching傳統靜態批次(例如以前用 Triton 的 Static Batching)最大的浪費是 “token 級空轉”——一批 token 請求只需要 5 個 token,其他要 50 個,那前 45 個 token 生成時, 比如說,這個位置還活得了,那麼結束Continuous Batching 的關鍵是 “按推理步調度”,每生成一個 token(也就是走一步推理)就重新排一次隊:
把那些 Early-Finished(早結束)的請求踢出去騰位置,再把新進來的 Late-Joining(晚加入)請求塞進去,這樣 GPU 的計算單元每一步都滿滿,沒有每一步都滿滿。
但這裡不是隨便塞,得處理好請求長度差異,不然短請求會被長請求堵死,所以現在大多會分 “長度桶”,把差不多長度的請求放一個桶裡調度,減少調度出現碎片化(長短不一導致的),比如 100-200token 的放一桶,200-300 的延遲,另一桶,另一壓排放,各自既安全又不加調度。
Continuous Batching適合處理不同長度的prompt,因為大模型推理時,有的請求可能只需要5個token就完成,有的可能需要50個,傳統批處理會為了最長的那個請求等很久,而Continuous Batching可以實時調整,讓GPU一直在幹活,不閒著。
它在每次GPU推理的間隙,也就是”寫完一個token的空檔”,偷偷做點調度工作。系統會即時監控每個請求的進度,一旦發現某個請求提前寫完了(Early-Finished),就把它從當前批次中移除,騰出位置給新來的請求。同時,當新請求到達時(Late-Joining),它也能立即插入到目前批次中,不需要等待目前批次完成。

(二)串流式生成再看流式互動式生成,串流生成其實是 KV-Cache 在服務層的另一種應用,它不只是 「邊生成邊回饋」 這麼簡單。
傳統非流式是等模型把所有 token 都算完再返回,這裡有兩個大問題:
一是用戶等得久,
二是 “資源佔用長尾”:1個要生成 100 個 token 的請求,得佔著 KV-Cache 整整 100 步,期間其他請求進不來。
流式生成= 增量解碼 + 中途打斷,生成一個 token 就返回一個,同時把這個 token 對應的臨時計算結果(不是 KV-Cache,KV-Cache 還得留著算下一個)釋放掉,更重要的是用戶能隨時喊停,比如模型生成到第 10 個 token,用戶覺得不對,直接終止了 90 省而且底層必須跟 KV-Cache 聯動,因為每一步只算當前 token,得靠 KV-Cache 調出前面的歷史信息,不然每一步都得重算前面所有 token,反而更慢。
(三)長序列推理長序列推理這塊,痛點是傳統 Transformer 的 Attention 計算是 O (n²),n 是序列長度,n 從 1k 漲到 10k,計算量直接從 1e6 飆到 1e8,顯存和算力都扛不住。
所以所有長序列優化,本質都是–如何在不失去關鍵訊息的前提下,把 O (n²) 降到 O (n) 或 O (nk)(k 是某個小常數)。
英偉達的 Star Attention,它賭的是 「局部性假設」—— 當前 token 主要跟附近的 token 有關,讀文章時,一句話裡的詞大概率只和前後幾句關聯,不用跟開頭的詞扯關係。
所以它分兩階段:第一階段把長序列切成小塊,每個塊只算自己和前一個塊的 Attention,抓 “短期依賴”,這一步計算量是 O (nk)(k 是塊大小);第二階段用全局 Attention 把所有塊的結果拼起來,全局 “長期依賴”,但控制計算時只算塊級的特徵,對於每個 token,所以總量不是可控。例如 k=512,n=10k,計算量直接降 10 倍,還能保住長距離關聯。
Star Attention 的兩階段計算:第一階段是 “局部注意力”,把長序列切成大小為 k 的區塊。例如 n=10000,k=512,那大概可以切成 10000÷512≈20 個塊(取整數方便算)。
每個區塊只關注 「自己內在的 token」 和 「前一個區塊的 token」(抓短期依賴)。

每個區塊有 k 個 token,前一個區塊也有 k 個,所以每個 token 需要和 k(自己區塊)+k(前一個區塊)=2k 個 token 算注意力。
那一個區塊的計算量就是:k 個 token × 2k 次計算 = 2k²。

20 個區塊的總計算量就是:20 × 2k² = 40k²。代入 k=512,就是 40×512×512≈1048 萬次。
第二階段是 “全局注意力”,抓住長期依賴。這時候不細算每個 token 了,而是給每個區塊提一個 「總結特徵」(例如用區塊內所有 token 的平均值或最大值當代表),20 個區塊就有 20 個總結特徵。
全局注意力就是這 20 個特徵之間互相計算,計算量是 20×20=400 次(和第一階段比幾乎可以忽略)。
所以 Star Attention 的總計算量≈第一階段的 1,048 萬次 + 第二階段的 400 次≈1,048 萬次。

傳統 Attention 是 1 億次,1 億 ÷1048 萬≈9.5,差不多 10 倍壁仞的 “分段 + 狀態向量” 更雞賊,狀態向量其實是前一段的 “語義摘要”,就像你讀論文記段落大意,不用記每句話,後面段落基於大意算,不用算每句話回頭算每句話的 Attention。這裡的關鍵是狀態向量得小,例如把前一段 1000 個 token 的 Attention 輸出壓縮成 256 維向量,既省空間又能保留關鍵訊息,不然存狀態向量也佔顯存。 FlexPrefill 的 「動態調整範圍」 是 「注意力稀疏化」 的變種,它不是固定區塊大小,而是靠一個輕量的 「語意分析器」 判斷目前內容的 「密度」— 遇到 「綜上所述」 這種總結句,就擴大 Attention 範圍,關聯性分析器」 判斷目前內容的 「密度」- 遇到 「綜上所述」 這種總結句,就擴大 Attention 範圍,關聯性分析器」 遇到 「例如」這個舉例關聯, 這種總結句,就擴大 Attention 範圍,關聯性的更安全; 「以存換算」 技術:利用顯存和磁碟存儲,把不常用的資料放到顯存外,需要時再調回來。

我們先搞清楚 “存什麼換什麼”,存的是不常用的 KV-Cache 或 Attention 權重,換的是顯存空間,但得控制換入換出的延遲,比如把前 1000 個 token 的 KV-Cache 放到 SSD,但不能等要用了才調,得前 GPU 等內存,

二、推理引擎優化

(一)KV-Cache機制大模型推理的傳統方法每次生成新token都要重新計算所有歷史token的鍵值對,就像每次寫新句子都要重新讀一遍前面所有內容,這太浪費時間了。

KV-Cache 你可能知道是存 K 和 V,但為什麼是 K 和 V,不是 Q?因為 Q 是目前 token 的查詢,每次都不一樣,沒法存;K 是歷史 token 的 「鍵」(特徵標識),V 是歷史 token 的 「值」(資訊內容),這兩個是固定的,生成新 token 時只需要用新 Q 去查舊 KV,所以能存。

而且 KV-Cache 的儲存格式不太一樣,一般按註意力頭拆分,每個頭的 KV 單獨存,這樣後面做 TP(張量並行)時,能直接把每個頭的 KV 切分到不同 GPU,不用跨卡傳數據,省通信量。
還有動態擴容問題,一開始不知道請求要生成多少 token,不能預分配太大空間(浪費),也不能太小(不夠用要擴容,拷貝數據費時間),所以現在都用 “預分配小塊 + 動態拼接”,比如每個請求先給 64 個 token 的 KV 塊,不夠了再追加,拼接”,比如每個請求先給 64 個 token 的 KV 塊,不夠了再追加,內存碎片少,擴容快。
華為的 UCM 技術是 KV-Cache 的升級版,搞 “三級緩存”:熱數據(最近用的)放顯存,溫數據(近期可能用的)放內存,冷數​​據(很久不用的)放 SSD,還會跟踪每個 KV 塊的訪問頻率,比如 5 分鐘沒碰過就丟 SSD,1分鐘內碰過就放內存,這樣既省顯存,又能透過預取減少調數據的延遲,比如在當前計算快用完顯存時,提前把下一段需要的 KV 從 SSD 調到內存,不讓 GPU 等。
(二)PagedAttentionPagedAttention 是解決 KV-Cache 的 「記憶體片段化」 痛點,傳統 KV-Cache 要分配連續顯存,例如一個請求要 100 個 token,得找一塊 100token 的連續空間,要是顯存裡有 10 個 token,得找一塊 100token 的連續空間,要是顯存有 10 個 10token 的空閒塊,也只能用空塊。
PagedAttention 學作業系統的記憶體分頁,把 KV-Cache 切成固定大小的 「頁」(例如每頁存 64 個 token 的 KV),一個請求的 KV 不用連續存,分散在多個頁裡,用 “頁表” 記每個頁的位置,這樣不管空閒頁在哪,數量夠用,徹底解決碎片。例如一個請求要 100 個 token,拿 2 個頁(64+36,第二頁只用 36 個位置)就行,不用管這兩個頁在顯存的哪個角落。
而且它還支援 “頁共享”,例如兩個請求有相同的前綴(例如都用同一段文檔當上下文),它們的前綴 KV 頁能共享,不用存兩份,這就跟後面的 APC 聯動上了。淘汰策略也不是簡單 LRU,是 “引用計數 + LRU”,例如一個頁被 3 個請求共享,引用計數是 3,只有所有請求都不用了,計數歸 0,才會被淘汰,不會誤刪共享頁。 (三)自動前綴快取(APC)自動前綴快取 (APC) 解決的是 “前綴重複計算” 的大坑,例如 100 個用戶問 “這份 2025 財報的利潤是多少”“這份 2025 財報的營收是多少”,前面都帶相同的 1000 字 0 100 次,純浪費。
APC 的核心是 “前綴特徵哈希 + 頁共享”,不是直接對 token 哈希,而是對前綴對應的 KV 頁特徵做哈希(比如把每個 KV 頁的特徵壓縮成哈希值),這樣即使前綴略有不同(比如 “2025 財報” 和 “2025 年度財報”),也能匹配到相似 KV 頁,提高命中率。
而且它能 “動態匹配子集”,例如前綴 A 是 “財報第一章”,前綴 B 是 “財報第一章第一節”,APC 能認出 B 是 A 的子集,直接共享 A 裡第一節的 KV 頁,不用重算。 vLLM 裡的APC 還跟請求隊列綁在一起,新請求進來先查哈希表,有匹配的緩存就直接加載,跳過最費算力的 prefill 階段(計算前綴 KV 的階段),直接進 decode 階段(生成回答 token),prefill 階段本來就佔直接算力的 70% 以上,所以 APC 一上,吞吐量好幾漲好幾倍。另外,APC 得處理 “快取過期”,例如財報更新了,舊快取就得刪,所以每個快取區塊都有 “版本號” 和 “時間戳”,文件一更新,自動淘汰舊版,避免回傳錯誤資料。
(四)分散式推理並行方案TP和PP這兩種分佈式推理並行方案其實是解決推理時”人多力量大”但又”別搶著幹活”的問題。
TP(張量並行)是把一個大蛋糕切成多塊,每塊由不同的人負責切,但最終要拼成一個完整的蛋糕。
具體來說,TP把模型的張量(例如多頭注意力中的每個頭)分散到多個GPU上,每個GPU只負責一部分計算,例如MHA並行就是把多頭注意力的每個頭切分到不同GPU,KV快取也跟著切分。
這樣,當模型很大時,單一GPU放不下,TP就能把一個大運算任務拆成小塊,讓多個GPU一起工作,避免了單卡記憶體不足的問題,而且還能同時處理KV快取的切分,讓推理效率更高。
PP(管線並行)把整個模型依層切成多個階段,每個GPU負責一個階段的計算,就像生產線上的不同工位一樣。
例如第一層在GPU1上算,算完後把結果傳給GPU2算第二層,GPU1空閒了又能接新任務。這樣當請求量大時,GPU1可以繼續處理新請求,而GPU2還在處理上一個請求的後續層,大大減少了設備空閒時間。
PP最厲害的是它通訊量小,因為只傳層間的啟動值,KV快取是獨立的,不需要跨裝置傳遞,所以能支援更大的batch_size,因為單一GPU只存模型的一部分,節約下來的顯存可以存更多KV快取。
TP 是 “水平切分”,將同一層的模型權重拆到多卡,例如 16 個頭的 Attention,用 4 卡 TP,每卡負責 4 個頭,解決 “單卡裝不下大模型” 的問題。但 TP 有上限,並行度不能超過模型的 “可切分維度”,例如只有 16 個頭,最多 16 卡 TP,再多就沒法切了,所以適合中小規模並行。
PP 是 “垂直切分”,把不同層拆到多卡,例如 100 層模型,用 10 卡 PP,每卡負責 10 層,解決 “層數太多計算慢” 的問題。
PP 的關鍵是減少 “流水線氣泡”,剛開始卡 2 要等卡 1 算完才動,有個空窗期,現在都用 “重疊通信”,卡 1 要等卡 1 算完才動,有個空窗期,現在都用 “重疊通信”,卡 1 算完一層就把中間結果傳給卡 2,不用等 10 層都算完,氣泡就小算了。 TP 和 PP 可以這樣混合用:先按 TP 把每層拆成 M 卡,再按 PP 把 M 卡一組的層拆成 N 組,總共 M*N 卡,例如 4 卡 TP+2 卡 PP=8 卡,既能裝下大模型(TP 解大小),又能提速(PP 解層數多)。
華為的 「1 卡 1 專家」 是 MoE 模型的並行,每個專家放一卡,TP 切分單一專家的權重,PP 切分 MoE 的層,既用 MoE 的稀疏性(每次只激活部分專家),又解決專家數量多的問題。
DistriFusion 是影像產生的並行,把影像區塊以空間切分,每卡算一個區塊,減少邊緣通信,本質是 TP 的變種。
(五)算子融合優化算子融合其實就是讓神經網路的運算流程變得更緊湊,你做菜的話,不可能每切一塊肉就要洗一次刀,換一次砧板。算子融合就是把切肉、洗刀、換砧板這些步驟合併成一個連貫的動作,直接把肉切好就下鍋,不用來回折騰。
算子融合不是簡單 “合併步驟”,而是抓 “顯存頻寬瓶頸”。每個算子啟動一次 CUDA Kernel 要花幾十微秒,兩個算子啟動兩次就多一筆開銷,這還是小事。
更要命的是沒融合時,Add 的輸出要寫回顯存,LayerNorm 再從顯存讀,一寫一讀佔兩次頻寬,顯存頻寬本來就不夠,這一堵就慢了。
融合後,Add 的輸出直接在 GPU 的暫存器(或共享記憶體)傳給 LayerNorm,不用碰顯存,少了兩次頻寬佔用,還省了一次 Kernel 啟動開銷。
但融合得看 “依賴和相容性”,例如 Add 和 LayerNorm 是前後依賴,還在同一維度操作,才能合。
這種優化不改變計算邏輯,只是把計算流程重組了,只是把我們組員的工位合併,不用來回走動。
在Transformer這種結構裡,這種融合特別有效,因為像Add和LayerNorm這種操作本來就是前後依賴的,完全可以合併。
FasterTransformer 把 MultiHeadAttention 的整套流程(QKV 投影→Scaled Dot-Product→多頭拼接→線性層)都融合成一個 Kernel,原來要啟動 5-6 次 Kernel,現在一次搞定,中間資料全在共享內存裡傳,顯存訪問少 80% 以上,速度直接翻倍。
三、模型量化技術最後是模型量化,做到平衡精度損失和節省資源。
大模型的坑在於活化值和權重分佈極不均勻,例如活化值裡可能有幾個 100 的離群值,大部分在 – 1 到 1 之間,普通對稱量化會把範圍拉到 [-100,100],小值全被量化成 0,失真嚴重。
SmoothQuant 的辦法是 “轉移誤差”,給活化值乘 0.1 縮到 [-10,10],權重乘 10 擴到 [-100,100],讓兩者量化範圍接近,失真變小。
LLM.int8 是 “混合精度”,權重用 INT8,激活值裡的離群值用 FP16,因為離群值少但影響大,這樣既省資源又保精度,比如 99% 的數用 INT8,1% 用 FP16,顯存只多一點,精度跟 FP16 差不多。
GPTQ 更狠,量化前先微調,把權重切成小塊,每塊量化後算誤差,再調整未量化的權重補償誤差,相當於 “瘦身時補營養”,讓模型瘦了還有力氣。
例如 16B 模型 FP16 要 32GB 顯存,INT4 量化後只要 10GB,普通顯示卡就能跑,batch_size 還能翻倍,速度快 4 倍以上。寒武紀的 vLLM-MLU 用 INT8,是因為 MLU 的 INT8 計算單元比 FP16 多,例如每個核心有 1024 個 INT8 ALU,只有 256 個 FP16 的,不用 INT8 就浪費硬體算力。
另外,量化還得防 “溢出”,INT8 範圍是 – 128 到 127,計算前得把輸入縮到位,不然一溢出結果就錯了,現在的量化算子都有動態縮放,就是做這件事情的。


發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *