化:Prefill 與 Decode 兩階段瓶頸及調(diào)優(yōu)實踐)
你大概見過這種曲線服務(wù)剛上線時首 token 延遲兩百毫秒上下看著挺體面并發(fā)一上來這個數(shù)字開始抖抖到兩秒、五秒可每 token 的輸出延遲卻穩(wěn)得很四五十毫秒沒怎么變。運維查了網(wǎng)卡、查了顯存占用、查了卡上的利用率指標(biāo)都不難看但用戶就是在反饋卡。遇到這種情況我一般先不碰硬件也不急著換推理框架而是把一次請求拆成兩個性質(zhì)完全不同的階段來看——**Prefill預(yù)填充**和Decode解碼。這兩個詞在大模型推理里出現(xiàn)的頻率極高但真正搞清楚它們在計算上差在哪、為什么要用兩套策略去伺候的人并不多。Prefill 是一次性把整段 prompt 塞進網(wǎng)絡(luò)一次前向算完所有 token 并行過模型Decode 是每步只吐一個 token一步一循環(huán)直到遇到結(jié)束符。前者是典型的算力密集型后者是典型的訪存密集型兩者的瓶頸、優(yōu)化手段、調(diào)參方向幾乎是相反的。你把它們當(dāng)成一件事來調(diào)用同一套批處理策略、同一個顯存預(yù)算、同一種量化方案結(jié)果必然是兩頭不討好。這篇內(nèi)容就是把這層窗戶紙捅破從矩陣乘的形態(tài)講到顯存帶寬的賬從 KV Cache 怎么長出來講到分塊預(yù)填充和 PD 分離怎么落地中間插一些我自己踩過的坑和可以直接抄的壓測腳本。不需要你先懂 CUDA 算子只要你會寫幾行 Python、跑過模型服務(wù)就能順著往下讀如果你已經(jīng)在做推理部署第 4、5 部分應(yīng)該能直接對上你監(jiān)控里的那些異常曲線。1. 把兩個階段掰開看一次請求到底發(fā)生了什么1.1 從輸入到第一個 tokenPrefill 到底干了什么用戶發(fā)來一句話比如三百個 token推理引擎收到的是一整個 token 序列而不是一個一個喂進去。Prefill 做的事情就是把這個長度 300 的序列當(dāng)成一個整體走一遍完整的前向embedding 查表得到 300 個向量每一層做自注意力、做前饋網(wǎng)絡(luò)一路往上最后只取最后一個位置的輸出去算下一個 token 的概率分布。注意這里的關(guān)鍵——300 個位置全部參與了計算但它們之間不是獨立的自注意力里的每個位置都要看到它前面所有位置的信息所以這一層的計算量隨序列長度是平方級增長的。用生活化的說法Prefill 像是一次性把一整篇文章讀進去理解讀的時候前后文互相對照讀完整篇才寫出第一個字。這個過程里模型權(quán)重只需要被讀取一遍但是要參與大量的乘加運算所以它對算力的胃口極大對顯存帶寬反而不那么敏感。這也是為什么在 Prefill 階段你會看到加速卡的算力利用率SM 占用、Tensor Core 活躍度能跑到比較高的水位而顯存控制器沒那么忙。還有一點容易被忽略Prefill 階段要一次性寫入全量 KV Cache。300 個 token每一層都要存下它們的 Key 和 Value這些數(shù)據(jù)后面 Decode 階段要反復(fù)讀取所以 Prefill 不只是算還是一次大規(guī)模的顯存寫操作。這個寫的量級我在 2.4 節(jié)會算給你看先記住這個動作的存在。1.2 從第二個 token 開始Decode 為什么是一個繞不開的循環(huán)第一個 token 吐出來之后流程就變了。第二步的輸入只有一個 token上一步生成的那個但它要跟前面 300 個 token 的 KV 拼在一起做注意力。這里就體現(xiàn)出 KV Cache 的價值了前面 300 個 token 的 K 和 V 在 Prefill 階段已經(jīng)算好存下來了Decode 每一步只需要算當(dāng)前這一個 token 的 Q、K、V然后把新 K、V 追加到緩存尾部再用當(dāng)前 Q 去跟全部 301 個位置做注意力。如果沒有 KV Cache每一步都要把全部歷史重新算一遍代價會大到?jīng)]法用。關(guān)鍵是這個動作要重復(fù)幾百上千次。每生成一個 token你就要把整個模型的權(quán)重從頭到尾讀一遍但在當(dāng)前這一步里真正參與計算的新 token 只有一個。權(quán)重讀了幾十 GB算的卻只是一次矩陣乘向量這就是 Decode 階段的根本矛盾——算力大量閑置帶寬被榨干。你可以把它想象成每次只為了查一個字就把整本字典從頭翻一遍。字典的內(nèi)容權(quán)重沒有變翻的動作顯存讀取卻是實打?qū)嵉某杀尽?.3 兩個階段的本質(zhì)差異用一張表說清楚把上面的描述收斂成對照表后面所有的優(yōu)化手段幾乎都能從這張表里推出來維度Prefill預(yù)填充Decode解碼輸入形態(tài)長度 T 的完整序列單 token每步計算量與 T 成正比線性層 與 T2 成正比注意力每步固定與歷史長度成正比注意力部分權(quán)重讀取一次前向讀一遍每生成一個 token 讀一遍核心瓶頸算力compute-bound顯存帶寬memory-bound矩陣形態(tài)大 GEMM瘦高矩陣乘胖矩陣GEMV 退化矩陣乘向量KV Cache批量寫入逐步追加、全量讀取批處理收益中等主要攤薄權(quán)重讀取極高權(quán)重讀取被 batch 攤薄優(yōu)化重點提高算力利用率、減少無效計算提高帶寬利用率、增大有效 batch量化敏感度中注意激活值精度高權(quán)重位寬直接決定速度這張表建議你貼在工位上。后面遇到任何優(yōu)化方案先問一句它作用在哪一列答案清楚了方案值不值得試也就清楚了。比如有人跟你說把權(quán)重量化到 4bit 能提速一倍你心里應(yīng)該有數(shù)這是沖著右列去的對 Prefill 的收益非常有限甚至可能因為反量化開銷反而變慢。2. 算力賬本為什么 Prefill 吃算力、Decode 吃帶寬2.1 算術(shù)強度一個能解釋大部分現(xiàn)象的比值要判斷一段計算是吃算力還是吃帶寬標(biāo)準(zhǔn)工具是算術(shù)強度Arithmetic Intensity定義很簡單每從顯存里搬一個字節(jié)的數(shù)據(jù)能順帶做多少次浮點運算。單位是 FLOP/Byte。拿它去和硬件的脊點Ridge Point比脊點等于硬件峰值算力除以峰值帶寬超過脊點的計算就是算力受限低于脊點的就是帶寬受限。拿一塊常見的數(shù)據(jù)中心加速卡舉例FP16 稠密算力按 300 TFLOPS 估顯存帶寬按 2000 GB/s 估脊點大約是 150 FLOP/Byte。意思是每搬 1 個字節(jié)的數(shù)據(jù)你得做夠 150 次浮點運算才算沒浪費這塊卡的算力?,F(xiàn)在看 Prefill。假設(shè) 7B 參數(shù)模型輸入 2048 個 tokenFP16。權(quán)重總量約 13.4 GB只讀一遍計算量按經(jīng)驗公式 2 × 參數(shù)量 × token 數(shù)估2 × 6.7e9 × 2048 ≈ 2.74e13 FLOPs也就是 27.4 TFLOP。算術(shù)強度 2.74e13 / 1.34e10 ≈ 2045 FLOP/Byte。遠遠高于 150妥妥的算力受限。所以 Prefill 該做的事是把算力榨干而不是省顯存讀取。再看 Decodebatch 1。每生成一個 token權(quán)重還是讀 13.4 GB計算量只有 2 × 6.7e9 ≈ 1.34e10 FLOPs。算術(shù)強度 1.34e10 / 1.34e10 1 FLOP/Byte。連脊點的百分之一都不到。這就是為什么單請求跑 Decode 的時候你看顯卡的算力利用率會低得離譜而顯存帶寬可能已經(jīng)跑滿。這組數(shù)字帶來的直覺是Decode 階段要提升吞吐唯一的出路是把 batch 做大讓同一份權(quán)重讀取服務(wù)多個請求把算術(shù)強度拉回來。batch 16 時算術(shù)強度能到 16雖然離脊點還遠但吞吐已經(jīng)是 batch 1 的十幾倍了。2.2 Prefill 的 FLOPs 拆解線性層和注意力各占多少很多人以為 Prefill 的耗時大頭永遠是注意力這個認(rèn)知在短上下文下是錯的在長上下文下才對。我們把 2.74 TFLOP 拆開看。線性層部分QKV 投影、輸出投影、FFN 的兩層總共接近 2 × 參數(shù)量 × T就是上面算的 27.4 TFLOP 里的大頭。注意力部分單獨算QK^T 的乘加是 2 × T2 × d加權(quán)求和 AV 也是 2 × T2 × d兩層注意力矩陣乘加起來 4 × T2 × d再乘層數(shù) L。代入 T 2048、d 4096、L 324 × 20482 × 4096 × 32 ≈ 2.2e12也就是 2.2 TFLOP。占比約 8%。但注意力是平方增長的線性層是線性增長的。把 T 拉到 8192注意力部分乘 16 變成 35 TFLOP線性層只乘 4 變成 110 TFLOP注意力占比升到 24%。再拉到 32768注意力變成 560 TFLOP線性層 440 TFLOP注意力占比過半。這份賬解釋了為什么同一個服務(wù)短 prompt 場景跑得好好的一接長文檔場景就崩。不是你的框架不行是計算結(jié)構(gòu)變了原來可以忽略的那一項變成主項了。對應(yīng)的優(yōu)化手段也就不一樣短上下文該優(yōu)化 GEMM 的分塊和融合長上下文得先想辦法把注意力的平方項壓下去。2.3 Decode 的訪存賬本單卡吞吐的理論上限D(zhuǎn)ecode 每一步要讀什么權(quán)重 13.4 GB加上當(dāng)前上下文的 KV Cache。上下文 2048 時KV Cache 大小在 1 GB 量級算法見下節(jié)總計約 14.4 GB。顯存帶寬 2000 GB/s理論耗時 14.4 / 2000 ≈ 7.2 毫秒換算成單請求速度約 139 token/s。這是理論天花板實際因為流水線氣泡、kernel 啟動開銷、帶寬利用率達不到 100%通常是這個數(shù)字的三分之一到二分之一也就是 40 到 70 token/s。你線上看到的單請求解碼速度基本就在這個區(qū)間。再看 batch 16。權(quán)重還是讀 13.4 GB但 KV 變成 16 × 1 GB 16 GB總計 29.4 GB耗時 14.7 毫秒。注意這 14.7 毫秒里16 個請求各自都推進了一個 token所以吞吐是 16 / 0.0147 ≈ 1088 token/s是單請求的 7.8 倍。這就是連續(xù)批處理的價值所在。順便說個反直覺的現(xiàn)象batch 增大時單個請求的 TPOT每 token 延遲實際上是變差的。batch 1 時 7.2 msbatch 16 時每個請求還是要等 14.7 ms 才拿到自己的那個 token。吞吐和延遲在 Decode 階段是明確的 trade-off不是雙贏。這一點在 4.1 節(jié)還要展開。2.4 KV Cache 的顯存賬被大多數(shù)人低估的那部分KV Cache 到底多大公式是每 token 每層存一對 K 和 V每個張量的元素數(shù)是 n_kv_heads × head_dim數(shù)據(jù)類型 FP16 占 2 字節(jié)。所以每 token 大小 2K 和 V× n_kv_heads × head_dim × 2 字節(jié) × 層數(shù)以 32 層、32 個 KV 頭、head_dim 128 的模型為例2 × 32 × 128 × 2 × 32 524288 字節(jié)正好 512 KB/token。2048 個 token 就是 1 GB8192 個 token 就是 4 GB32768 token 就是 16 GB已經(jīng)超過模型權(quán)重本身了。如果模型用了分組查詢注意力GQA把 KV 頭從 32 組減到 8 組每 token 直接降到 128 KB同樣的顯存能撐四倍上下文。這是現(xiàn)在主流模型幾乎都上 GQA 的核心原因省的不是算力是 KV Cache。做容量規(guī)劃時可以這樣估一塊 80 GB 顯存的卡模型權(quán)重 13.4 GB留給 KV 的按 60 GB 算還要留激活值和臨時緩沖區(qū)如果每 token 128 KB能裝下約 46 萬個 token 的總量。這個總量是所有并發(fā)請求共享的8 個并發(fā)各 8192 上下文就用掉 65 萬個 token直接爆。這就是為什么長上下文并發(fā)一高就 OOM而單純把 batch 調(diào)小只是把問題往后推。3. 算子視角從 GEMM 到 GEMV 的性能落差3.1 Prefill 階段是大矩陣乘的堆疊Prefill 里最主要的計算形態(tài)是 GEMM通用矩陣乘。以 FFN 層為例輸入是一個 [T, d] 的矩陣權(quán)重是 [d, 4d] 的矩陣輸出 [T, 4d]。T 動輒幾百上千兩個維度都足夠大GPU 的 Tensor Core 可以做很好的分塊數(shù)據(jù)復(fù)用率高算力能跑到峰值的相當(dāng)比例。QKV 投影同理輸入 [T, d] 乘權(quán)重 [d, 3d]也是大 GEMM。這類計算的優(yōu)化方向很成熟算子融合把 QKV 三個投影合成一個、分塊大小調(diào)優(yōu)、用更寬的累加精度。你去看推理框架的日志Prefill 階段耗時基本都花在少數(shù)幾個大 kernel 上每個 kernel 的耗時是可以預(yù)測的優(yōu)化空間主要在于減少 kernel 數(shù)量和減少中間結(jié)果的顯存往返。3.2 Decode 階段退化成矩陣乘向量Decode 的輸入只有一個 token也就是 [1, d] 的向量去乘 [d, 4d] 的權(quán)重矩陣。這在形態(tài)上叫 GEMV矩陣乘向量。問題是 GEMV 的訪存模式極不友好權(quán)重的每一行只用一次沒有任何復(fù)用相當(dāng)于純粹把權(quán)重從顯存搬到計算單元搬完就扔。這在 GPU 上是最浪費的場景之一。這也是為什么很多框架在 Decode 階段會做批量化 GEMV把 batch 個請求的向量拼成 [B, d]退化成一個小 GEMM。B 越大越接近 GEMM 的效率。但 B 受限于顯存和延遲要求實際很難做大通常在 8 到 64 之間徘徊。還有一個經(jīng)常被忽視的成本kernel 啟動開銷。Decode 每生成一個 token都要走完全部層幾十個 kernel 挨個啟動。如果每個 kernel 只有幾十微秒啟動開銷和調(diào)度開銷的占比就很可觀了。所以 Decode 階段特別吃算子融合和CUDA Graph 捕獲這類手段把整段前向固化成一張圖一次性提交能省掉大量 CPU 側(cè)的調(diào)度開銷。這一點在 batch 小的時候尤其明顯我自己實測過只加圖捕獲這一項小 batch 場景的 TPOT 能降一到兩成。3.3 注意力部分兩種截然不同的數(shù)據(jù)流動Prefill 的注意力是一整塊下三角的因果掩碼矩陣。T 個 query 對 T 個 key 做注意力掩碼保證每個位置只能看到自己和前面。這個形態(tài)適合做分塊計算把一個 T×T 的大矩陣切成小塊只算下三角部分跳過被掩碼的上三角。分塊注意力的核心思路就是避免把整個 T×T 的注意力分?jǐn)?shù)矩陣物化到顯存里——那個矩陣在 T 8192 時是 81922 × 2 字節(jié) ≈ 128 MB 每頭每層物化出來直接爆。Decode 的注意力則簡單得多當(dāng)前這一個 query對長度為 L 的全部 key 做注意力。分?jǐn)?shù)向量長度是 L不涉及平方項。但它是純讀取操作——要把 KV Cache 里 L 個位置的數(shù)據(jù)全讀一遍。上下文 2048 時讀 1 GB耗時 0.5 ms看著不多上下文 32768 時讀 16 GB耗時 8 ms已經(jīng)超過讀權(quán)重的 6.7 ms 了。這時候 Decode 的瓶頸從權(quán)重帶寬變成了KV 帶寬。這就是長上下文解碼慢的底層原因不是模型變復(fù)雜了是每步要搬的數(shù)據(jù)變多了。對應(yīng)的手段有 GQA直接減少 KV 體積、KV 量化FP16 降到 INT8體積減半、稀疏注意力只讀部分 KV、以及滑動窗口限制窗口大小老 KV 直接丟棄。每一樣都是在拿精度或能力換速度沒有免費的午餐。3.4 分頁式 KV 管理與前綴復(fù)用傳統(tǒng)做法是給每個請求預(yù)分配一塊連續(xù)的顯存放 KV長度按最大可能值留實際用不完也不給別人。這會造成嚴(yán)重的內(nèi)部碎片——一個請求只要 200 token你按 8192 預(yù)留剩下 7992 的空位全浪費了。當(dāng)并發(fā)上來顯存被一堆半空的塊占滿明明還有余量卻接不了新請求。分頁式管理借鑒了操作系統(tǒng)的虛擬內(nèi)存思路把 KV Cache 切成固定大小的塊比如每塊存 16 個 token 的 KV按需分配不要求物理連續(xù)用一張映射表記錄邏輯位置到物理塊的對應(yīng)關(guān)系。好處有三個碎片幾乎消失顯存利用率能到 90% 以上請求長度可以動態(tài)增長不用預(yù)留最重要的是塊可以共享。塊共享這點衍生出一個非常實用的功能前綴緩存。很多線上場景的 prompt 有大量相同前綴比如固定的系統(tǒng)提示詞、固定的知識庫文檔頭。這些前綴算出來的 KV 完全一樣沒必要每個請求都重算一遍。分頁管理下只要把相同前綴的塊標(biāo)成共享后續(xù)請求直接跳過這部分 Prefill只算新增的部分。我在一個客服類場景里試過系統(tǒng)提示詞加固定說明有 1500 token命中前綴緩存后首 token 延遲從 600 ms 降到 180 ms效果立竿見影。注意前綴緩存的前提是前綴逐 token 完全一致差一個字符都會失效。實際落地時要把動態(tài)內(nèi)容時間戳、用戶名挪到 prompt 末尾否則命中率會低得讓你懷疑人生。4. 工程實踐把兩階段調(diào)順手的幾個關(guān)鍵動作4.1 首 token 延遲與單 token 延遲的取舍衡量推理服務(wù)質(zhì)量兩個核心指標(biāo)必須分開看TTFTTime To First Token首 token 延遲主要由 Prefill 決定加上排隊時間TPOTTime Per Output Token每 token 延遲主要由 Decode 決定加上調(diào)度間隔。用戶體感上TTFT 決定點完之后多久有反應(yīng)TPOT 決定吐字流不流暢。這兩個指標(biāo)是互相打架的。你真要壓低 TTFT就得讓 Prefill 優(yōu)先跑把 Decode 請求掛起等著Preill 一跑就是幾十上百毫秒這期間所有正在解碼的請求全都停頓用戶看到的就是打字機卡住。反過來你為了保證 TPOT 平穩(wěn)就得限制 Prefill 的頻率和批量TTFT 自然上去。我的經(jīng)驗是做分檔交互式場景聊天、助手優(yōu)先保 TPOT把 Prefill 切碎混進 Decode 的批里TTFT 允許 300 到 800 ms批處理場景文檔摘要、批量翻譯優(yōu)先保吞吐Prefill 可以大批量跑滿TTFT 到幾秒也無所謂。這兩類流量最好在入口就做好路由別混在一個池子里否則兩邊的指標(biāo)都會難看。4.2 分塊預(yù)填充讓長 prompt 不再堵車分塊預(yù)填充Chunked Prefill是我認(rèn)為性價比最高的一個改動。做法很簡單把一個長 prompt 的 Prefill 拆成若干塊比如每塊 512 個 token然后每一輪調(diào)度時把一塊 Prefill 和當(dāng)前所有 Decode 請求拼成一個混合批一起跑一次前向。這樣做的效果是把原來一次獨占式的大 Prefill變成多次小塊 Prefill代價是 TTFT 稍微變長因為分了幾輪收益是 Decode 請求幾乎感受不到停頓TPOT 曲線會變得平滑。對于那些既要處理長文檔又要同時服務(wù)多個短對話的服務(wù)這個改動能直接把尾延遲壓下去一大截。參數(shù)選擇上有講究塊太小調(diào)度次數(shù)變多每次的固定開銷占比上升整體吞吐下降塊太大Decode 被擠占的時間變長TPOT 抖動回來了。我的經(jīng)驗是從 512 開始試觀察 TPOT 的 P99 和整體吞吐如果 P99 抖得厲害就往下調(diào)到 256如果吞吐掉得多就往上調(diào)到 1024。這個值和硬件、模型大小強相關(guān)沒有一個通用最優(yōu)解。4.3 連續(xù)批處理與顯存水位控制連續(xù)批處理Continuous Batching解決的是一個批里最快的請求要等最慢的這個浪費問題。傳統(tǒng)靜態(tài)批處理一批里 8 個請求7 個已經(jīng)生成完了在等第 8 個這 7 個占著顯存不干活。連續(xù)批處理的做法是每生成一步就檢查一次誰結(jié)束了立刻踢出去空出來的位置馬上塞新請求進來。這個機制對 Decode 階段意義巨大因為 Decode 的每一步是一個天然的調(diào)度點。但要注意一個細節(jié)新請求進來通常先要跑 Prefill如果它一進來就獨占正在解碼的請求又要停。所以連續(xù)批處理必須和分塊預(yù)填充配合使用否則等于把堵車點從入口挪到了中途。顯存水位控制是配套的另一個動作。引擎一般會設(shè)一個閾值比如顯存占用的 90%超過就拒絕新請求或者排隊。這個閾值不能設(shè)太滿因為 Prefill 的激活值峰值比 Decode 高很多——處理一個 8192 token 的 Prefill中間激活值可能要吃幾 GB如果你按 Decode 的用量把顯存占到 95%一個長 prompt 進來直接 OOM。我一般留 10% 到 15% 的余量并且對超過某個長度的 prompt 單獨做限流。4.4 量化與推測解碼分別作用在哪一段量化怎么影響這兩個階段值得單獨說因為很多人量化完發(fā)現(xiàn)沒提速以為是量化沒用其實是作用錯了地方。權(quán)重量化比如 4bit、8bit 只量化權(quán)重激活值還是 FP16主要收益在 Decode。因為 Decode 是帶寬受限權(quán)重從 16bit 降到 8bit讀取量直接減半速度理論上接近翻倍實測常見 1.4 到 1.8 倍。而 Prefill 是算力受限權(quán)重量化省下的帶寬它用不上反而要額外做反量化可能還慢一點。權(quán)重加激活全量化W8A8才有可能同時惠及 Prefill因為激活也變成低精度后矩陣乘可以用低精度的 Tensor Core算力翻倍。但這條路對精度的影響要仔細評估特別是激活值的離群點問題很多模型直接上 W8A8 會掉點得厲害需要配合平滑或裁剪手段。推測解碼Speculative Decoding是另一個思路用一個小模型快速草擬幾個 token再用大模型一次性并行驗證。它本質(zhì)上是把 Decode 的多個串行步驟轉(zhuǎn)化成一次 Prefill 形態(tài)的并行驗證——因為驗證階段可以把 k 個候選 token 當(dāng)成一個長度為 k 的序列一次性過模型這正好是 Prefill 的形態(tài)。所以推測解碼特別適合 Decode 帶寬打滿、算力閑置的場景能把閑置的算力利用起來。代價是要多一份小模型的權(quán)重顯存并且在草稿命中率低的時候收益歸零。4.5 一套可復(fù)現(xiàn)的壓測方法調(diào)參不能靠感覺得有一套能量化 TTFT 和 TPOT 的壓測。下面這段是我常用的骨架思路是流式接收、記錄首塊到達時間和總耗時然后取中位數(shù)和 P99 看穩(wěn)定性import time import statistics def bench_one(client, prompt, max_new_tokens128): t0 time.perf_counter() ttft None n_tokens 0 for chunk in client.stream(prompt, max_tokensmax_new_tokens): now time.perf_counter() if ttft is None: ttft now - t0 # 首塊到達 n_tokens 1 total time.perf_counter() - t0 # 減去 TTFT 得到純解碼耗時再除以生成的 token 數(shù) tpot (total - ttft) / max(n_tokens - 1, 1) return ttft, tpot def bench(client, prompt, runs30): ttfts, tpots [], [] for _ in range(runs): a, b bench_one(client, prompt) ttfts.append(a) tpots.append(b) ttfts.sort(); tpots.sort() p99 lambda xs: xs[int(len(xs) * 0.99) - 1] return { ttft_median: statistics.median(ttfts), ttft_p99: p99(ttfts), tpot_median: statistics.median(tpots), tpot_p99: p99(tpots), }壓測時有幾個細節(jié)必須控制住否則數(shù)據(jù)沒有可比性。第一固定輸入長度和輸出長度別讓模型自由發(fā)揮不然每次生成的 token 數(shù)不同TPOT 沒法比。第二并發(fā)梯度測試從 1 并發(fā)測到目標(biāo)并發(fā)每一檔都記錄你會看到一條清晰的曲線并發(fā)上升吞吐先漲后平TTFT 和 TPOT 單調(diào)上升拐點就是這套配置的合理水位。第三區(qū)分冷啟動和熱態(tài)第一次請求要加載權(quán)重、要編譯圖數(shù)據(jù)不能用。還有一點壓測客戶端本身別成為瓶頸。Python 的同步 HTTP 客戶端在高并發(fā)下會被 GIL 和連接管理拖垮測出來的延遲里有一大半是客戶端自己的開銷。并發(fā)超過幾十就要用異步客戶端或者多進程壓我吃過這個虧一度以為服務(wù)端有問題查了兩天才發(fā)現(xiàn)是壓測腳本自己在排隊。5. 問題排查實錄那些監(jiān)控看不出原因的卡頓5.1 癥狀速查表線上一旦出現(xiàn)延遲異常先對照這張表定位能省掉大量瞎猜的時間癥狀大概率原因優(yōu)先檢查項TTFT 高但 TPOT 正常Prefill 排隊、批太大、前綴緩存沒命中排隊深度、Prefill 批大小、緩存命中率TTFT 正常但 TPOT 抖動Prefill 搶占、塊大小設(shè)置不當(dāng)分塊預(yù)填充是否開啟、塊大小TTFT 和 TPOT 同時劣化顯存接近上限、觸發(fā)換頁或拒絕顯存水位、KV 塊分配失敗率長 prompt 直接超時激活值峰值 OOM、單請求限流最大輸入長度限制、顯存余量并發(fā)上不去但顯存沒滿KV 碎片、塊分配器配置過小塊大小、可用塊數(shù)量量化后吞吐沒變化量化只作用在權(quán)重瓶頸在算力側(cè)是 Prefill 還是 Decode 慢單請求快、多請求慢批處理未生效或 batch 上限太低連續(xù)批處理開關(guān)、最大批大小首字很快但中途卡住KV 緩存被回收、請求被搶占重算搶占策略、優(yōu)先級配置這張表不是窮舉但覆蓋了我遇到過的大部分情況。用法是從左列找最像的然后按右列逐項排除通常兩三輪就能定位。5.2 幾個反直覺的坑第一個坑加大 batch 不一定提升吞吐。很多人看到吞吐不夠就把最大批大小往大了調(diào)結(jié)果發(fā)現(xiàn)吞吐反而降了。原因是 batch 大到一定程度后KV Cache 的讀取量超過了權(quán)重讀取量邊際收益遞減更糟的是顯存接近上限塊分配失敗率上升觸發(fā)請求搶占和重算實際干了雙份活。我一般的做法是找到那條吞吐曲線的平臺段停在平臺的 80% 位置留出余量應(yīng)對突發(fā)。第二個坑Prefill 的耗時不是線性的。有人按每 token 均攤?cè)ス浪阌X得 4096 token 的 prompt 應(yīng)該是 2048 的兩倍耗時。實際不是因為注意力的平方項在長上下文下占比上升4096 的 Prefill 耗時可能是 2048 的兩倍半甚至三倍。做容量規(guī)劃時一定要按實際長度壓測別用線性外推。第三個坑流式輸出的第一個塊不一定是真首 token。有些客戶端或網(wǎng)關(guān)會先發(fā)一個空的心跳塊或者角色聲明塊你在客戶端計時會把這段時間算進 TTFT測出來的數(shù)字比服務(wù)端真實值高出一截。排查時要在服務(wù)端也埋一份計時兩邊對不上就說明中間有額外緩沖。這個問題很隱蔽因為曲線看起來只是TTFT 偏高你會一直往服務(wù)端查。第四個坑KV Cache 量化之后長上下文的精度下降比短上下文明顯得多。因為 KV 量化誤差會隨著讀取的位置數(shù)量累積上下文越長注意力加權(quán)時引入的偏差越大。短問答場景測不出問題一上長文檔就發(fā)現(xiàn)模型開始記不住前面的內(nèi)容。要做 KV 量化必須用長上下文任務(wù)做驗證不能只跑短 prompt 的評測集。第五個坑多卡張量并行的通信開銷在 Decode 階段占比極高。張量并行把每一層的矩陣乘切到多卡上每層要做兩次全歸約。Prefill 階段計算量大通信能被掩蓋Decode 階段每層的計算只有幾十微秒通信延遲直接暴露出來。所以你會看到 4 卡部署的 Prefill 快了 3 倍多Decode 只快了 1.5 倍。小模型做 Decode 尤其不劃算寧可單卡多實例。最后分享一個我覺得最實用的觀察方式把 TTFT 和 TPOT 畫成兩條隨時間變化的曲線疊上并發(fā)數(shù)。正常情況下三條線的形狀應(yīng)該是同步的——并發(fā)漲、兩條延遲漲、并發(fā)落、延遲落。如果是 TTFT 跟著并發(fā)漲但 TPOT 是平的問題在入口調(diào)度如果 TPOT 出現(xiàn)周期性尖峰而并發(fā)沒變多半是分塊預(yù)填充的塊太大或者在跑定期任務(wù)如果兩條線一起抖且幅度很大去看顯存水位十有八九是塊分配器在掙扎。這套看圖方法比翻日志快得多尤其是在你沒時間深挖的時候能讓你在幾分鐘內(nèi)把范圍縮到一兩個模塊。