對 Scaling Law 的信仰不僅驅動著我們在模型參數與資料規模上不斷突破,也同樣在不斷逼近基礎設施工程的極限,這一過程伴隨著不可避免的陣痛,我們稱之為「Scaling Pain」。
隨著大型語言模型應用從簡單對話全面轉向更複雜、更長流程的 Coding Agent 任務,我們的推理基礎設施迎來了前所未有的壓力,每天承受著數億次 Coding Agent 呼叫。過去幾週,部分使用者在運用 GLM-5 系列模型執行複雜 Coding Agent 任務時,遭遇了多種異常:亂碼、復讀,以及偶發的生僻字。這些問題在標準推理環境下並不存在,只有在高併發、長上下文的 Coding Agent 場景下才會觸發,很難穩定復現。
我們經過數週的推演、排查與壓測,最終定位並修復了幾個相互獨立的底層競態 Bug,並對其中所反映的系統瓶頸進行了針對性優化,顯著提高了推理系統的穩定性和效率。
我們將在這段探索中收穫的經驗與教訓與大家分享,一起克服 Coding Agent 推理的 Scaling Pain。
從線下復現到異常識別
自 3 月起,我們在 GLM-5 的線上監控和使用者回饋中觀察到三類異常現象:亂碼(garbled output)、復讀(repetition),以及生僻字(rare character)。這些現象表面上與長上下文場景下常見的「降智」相似,但由於我們並沒有上線任何降低模型精度的最佳化,一個更關鍵的問題是:異常究竟源於模型本身,還是源於推理鏈路?如果源於模型,異常會表現為針對特定輸入的穩定、可重複行為;反之,若異常與系統壓力或執行時狀態相關,則更可能指向推理基礎設施中的鏈路或狀態管理問題。
排查初期,我們先對使用者回饋的壞案例做本地回放,並將同一批請求重複推理數百次,但始終未能復現異常,說明機率上這很可能不是模型本身的問題。為進一步模擬線上環境的壓力,我們對線上日誌做去識別化處理,並盡可能保留原始併發分佈與請求時序,在本地進行全量回放。起初仍未復現異常,直到進一步調整預填充與解碼分離比例並持續提高系統負載,模擬高峰期的預填充堆積和解碼側鍵值快取壓力後,才在約每萬次請求中穩定復現 3 到 5 次異常。這種「與請求內容無關、與系統壓力相關」的特徵,說明問題可能來自高負載下的推理狀態管理。與此同時,線下復現的異常頻率仍低於線上回饋的頻率,說明現有檢測方法可能存在漏檢,或仍有部分觸發場景尚未覆蓋。
如何可靠識別異常輸出成為了新的挑戰。三類異常中,復讀相對容易檢測,而亂碼與生僻字比較棘手。我們嘗試過正則表達式、字元集匹配等啟發式方法,也嘗試過基於模型判別的方式,但前者存在明顯的漏判與誤傷,後者則難以滿足大規模消融實驗的效率要求。上述限制使異常檢測本身成為定位流程中的一個瓶頸。
圖1:推測解碼指標可以作為異常檢測的重要參考
在反覆分析推理日誌後,我們發現了一個意想不到的切入點:推測解碼(Speculative Decoding)指標可以作為異常檢測的重要參考。推測解碼原本是一個效能最佳化技術,先由草稿模型生成候選 token,再由目標模型校驗並決定是否接受,從而在不改變最終輸出分佈的前提下提升解碼效率。如圖 1 所示,我們觀察到,兩個指標(推測接受長度:目標模型連續接受的草稿 token 前綴長度;推測接受率:草稿 token 被接受的比例)在異常發生時呈現出穩定模式:
- 亂碼和生僻字:通常伴隨極低的推測接受長度,即草稿模型生成的候選 token 幾乎全部被目標模型拒絕,顯示目標模型所看到的鍵值快取狀態與草稿模型預期之間存在顯著偏差。
- 復讀:通常伴隨偏高的推測接受率,顯示損壞的鍵值快取可能使注意力模式退化,並將生成過程推向高信賴度的重複循環。
基於上述觀察,我們進一步實現了一套線上異常監控策略:當推測接受長度持續低於 1.4 且生成長度已超過 128 token,或推測接受率超過 0.96 時,系統主動中止當前生成,並將請求交由負載均衡器重試。該策略使推測解碼從單純的效能最佳化技術,拓展為輸出品質的即時監控訊號,成為後續消融實驗中的關鍵工具。
錯誤修復一:預填充與解碼分離架構下的鍵值快取競態
在觀察到異常輸出與併發壓力具有明顯相關性後,我們進一步分析其原因。透過對請求生命週期以及推理引擎中預填充與解碼分離執行時序的分析,我們發現該問題源於請求生命週期與鍵值快取回收與復用時序之間的不一致,從而引發的鍵值快取復用衝突。
1. 原因分析:非同步中止引發的鍵值快取復用競態
為限制尾端延遲,我們在推理引擎中引入了基於逾時的請求終止機制:當預填充階段未在規定時間內完成時,解碼側會對請求執行中止,並回收其佔用的鍵值快取資源。然而,該中止訊號未被正確傳播至預填充側,同時解碼側也缺乏判斷鍵值快取是否可安全回收與復用的充分資訊。因此,在解碼中止並將對應鍵值快取空間分配給新請求之後,先前已發起的遠端直接記憶體存取寫入以及正在執行的預填充計算仍持續執行,未被同步取消。
圖2:預填充與解碼分離場景下鍵值快取競態示意圖
圖 2 中展示了在預填充與解碼分離架構下,兩個請求在預填充與解碼之間互動的時序關係,以及由此引發的鍵值快取競態。
在初始階段,請求一被傳送至預填充節點一和解碼節點。由於排程或排隊等原因,請求一在預填充節點一側經歷了一段等待後才開始執行預填充前向傳播。與此同時,解碼側在一段時間內未收到對應的鍵值快取資料,觸發逾時機制,並對請求一執行中止。
隨後,解碼側回收請求一佔用的鍵值快取槽位,但沒有正確通知預填充節點一。緊接著,新請求請求二到達,並被分配至預填充節點二和解碼節點。由於記憶體復用策略,請求二被分配到與請求一相同的鍵值快取位址。預填充節點二開始執行預填充前向傳播並進行鍵值傳輸,並在較短時間內完成,使解碼側進入生成階段。
與此同時,預填充節點一側針對請求一發起的鍵值快取寫入仍在繼續,其資料會寫入已被請求二復用的顯示記憶體區域,從而覆蓋請求二的部分鍵值快取。最終,請求二在解碼階段讀取到被覆蓋的資料,導致生成結果異常。
2. 修復方案:鍵值快取釋放的時序一致性保證
為消除上述競態,我們在推理引擎中引入了更嚴格的時序約束,在請求終止與鍵值快取寫入完成之間建立顯式同步關係。
具體而言,解碼在觸發中止後,會向預填充側傳送通知。預填充僅在以下條件滿足時返回「可釋放」訊號:相關遠端直接記憶體存取寫入尚未開始,或所有已提交寫入均已完成。解碼僅在收到該確認後,才允許回收並復用對應的鍵值快取槽位。該機制確保鍵值寫入不會跨越顯示記憶體復用邊界,從而避免跨請求的鍵值快取覆蓋。
修復效果:該修復上線後,異常輸出的發生率由約萬分之十幾下降至萬分之三以下。結果顯示,在預填充與解碼分離架構中,需要對跨節點的資料傳輸與顯示記憶體復用建立明確的一致性約束,以避免類似問題。
錯誤修復二:分層快取載入時序缺失
Coding Agent 場景顯著提高了輸入長度(平均超過 70K token),同時伴隨較高的前綴復用率。這類負載使分層快取(多級鍵值快取)成為線上服務中的關鍵最佳化手段。然而,在鍵值快取換入與計算重疊執行的情況下,當前實作未能保證資料在使用前已完成載入,導致可能出現未就緒鍵值快取被存取的情況。
1. 原因分析:管線同步缺失導致的讀取先於就緒
透過對分層快取執行時序分析,我們將問題定位在直接記憶體存取分層快取的快取讀取路徑上。系統會從中央處理器記憶體非同步換入歷史前綴快取,並透過載入串流與前向串流的重疊執行來提高吞吐量。
如圖 3(a) 所示,載入串流負責載入鍵值快取與索引器快取,而前向串流依次執行索引計算與後續的稀疏注意力。理論上,前向串流中的索引器計算應在對應的索引器快取完成載入後才能啟動。然而,在原始實作中,該依賴未被顯式表達。
具體而言,索引器運算子在啟動時未對載入索引器快取的完成建立同步約束(圖 3 中紅色虛線區域)。因此,前向串流可能先於載入串流完成資料載入而開始執行,從而出現讀取先於就緒的存取模式,即在資料尚未完成載入時即被讀取。
該問題會導致索引計算基於不完整或未初始化的資料執行,進而影響後續稀疏注意力的計算結果,並最終反映為輸出異常。
圖3:分層快取讀取管線時序異常與修復示意圖
2. 修復方案:重構運算元管線的原子性
為解決上述問題,我們對分層快取的讀取管線進行了修改(如圖 3(b) 所示),在資料載入與計算之間引入顯式的同步約束:
- 顯式同步約束:在索引器運算元啟動前引入與載入串流的同步點,確保對應層級的索引器快取已完成載入。前向串流僅在資料就緒後才啟動計算,從而避免讀取先於就緒的存取。
該修復上線後,在相同負載條件下,由執行時序不一致引起的異常完全消失,系統行為趨於穩定。該修復已透過 Pull Request #22811 提交至 SGLang 社群。
最佳化:鍵值快取分層儲存 LayerSplit
上述兩個競態問題揭示了一個共同的系統瓶頸:在長上下文的 Coding Agent 服務場景中,預填充階段主導了系統效能。
為了控制預填充排隊帶來的生成首個詞元時間,我們引入了逾時中止;為了緩解預填充側鍵值快取容量壓力,我們引入了分層快取。在修復這些狀態一致性問題後,我們進一步回到瓶頸本身:如何提升預填充吞吐量、降低預填充側鍵值快取顯示記憶體壓力。為此,我們設計並實作了鍵值快取分層儲存方案 LayerSplit。
Coding Agent 負載通常呈現出上下文長度較長、前綴快取命中率較高的特徵。在這一場景下,預填充階段往往成為系統的主要效能瓶頸,因此上下文並行成為線上預填充節點的主要並行策略。然而,現有的 SGLang 開源實作存在鍵值快取冗餘儲存的問題,導致有限的鍵值快取容量成為圖形處理器計算資源使用率的限制因素。
圖4:LayerSplit、鍵值快取分層儲存方案
針對這一問題,我們設計並實作了一種鍵值快取分層儲存方案(LayerSplit)。在該方案中,每張圖形處理器不再儲存全部層的鍵值快取,而是僅持有部分層的鍵值快取(如圖 4(a) 所示),從而顯著降低單卡的顯示記憶體佔用。
在計算過程中,不同上下文並行節點按照圖 4(b) 所示的方式協同完成預填充:具體而言,持有某一層鍵值快取的節點會在執行注意力計算前,將該層快取廣播給其他相關節點。為降低通訊開銷,我們進一步設計了鍵值快取廣播與索引器計算的重疊機制,使二者在時間上相互掩蓋。最終,整個流程中僅引入了索引器快取廣播的額外開銷,其規模約為鍵值快取的八分之一,因此整體通訊成本較低,對效能影響可以忽略。
圖5:GLM-5.1 + LayerSplit 在不同長度下的吞吐量提升
圖 5 展示了在快取命中率達到 90% 的條件下,該最佳化在請求長度從 40k 到 120k 區間內帶來的效能提升。實驗結果顯示,系統吞吐量提升幅度在 10% 至 132% 之間,且隨著上下文長度的增加,收益更加顯著。整體來看,該最佳化顯著提升了系統在 Coding Agent 場景下的處理能力。
總結
當智慧真正進入高併發、長上下文的 Coding Agent 場景後,推理基礎設施的挑戰已經不只是吞吐量、延遲和可用性,維護它的輸出品質變得至關重要。每一次對 Scaling Law 的追求,都必須有同等強度的系統工程作為支撐。我們分享這些經驗,希望幫助社群少走一些冤枉路,共同打磨出能夠承載通用人工智慧未來的推理基礎設施。
致謝
這篇部落格介紹了我們對 Coding Agent 服務中一系列系統級問題的研究,包括這些問題的復現與分析,以及相應的最佳化方案。我們感謝中科加禾(北京)科技有限公司以及中國科學院計算技術研究所處理器晶片全國重點實驗室團隊的合作與支援。
技術部落格連結:https://z.ai/blog/scaling-pain