
AI漫劇的批量分鏡生成卡脖子問題基本不在生成質(zhì)量而在跑圖速度。我用SDXL在單張4090上出一張1024x1024的分鏡底圖20步采樣通常要七八秒看起來單張不算慢但一個劇本拆出幾百個分鏡、每個分鏡還要出好幾張候選圖時間賬一下子就難看了。后來我把CUDA Graph的捕獲與重放機制用到SDXL的UNet采樣循環(huán)里單張耗時可觀下降而且不用改模型結(jié)構(gòu)、不動權(quán)重風(fēng)險很小。這篇文章把我的實踐過程、踩過的坑和收益數(shù)據(jù)都整理出來給同樣做批量出圖、卡在推理延遲上的朋友做個參考。1. 項目背景AI漫劇為什么會被SDXL的推理速度卡住1.1 漫劇生產(chǎn)鏈條里的“分鏡流水線”漫劇的生產(chǎn)邏輯和傳統(tǒng)漫畫不一樣它更接近“動畫分鏡”的流水線拿到文字腳本后先拆成一場場分鏡每個分鏡再轉(zhuǎn)成具體的人物、動作、背景描述最后批量生成底圖。這一套流程里SDXL承擔(dān)的是最重的那一塊生成構(gòu)圖合理、細(xì)節(jié)豐富的底圖。LoRA負(fù)責(zé)角色一致性ControlNet負(fù)責(zé)卡住構(gòu)圖結(jié)構(gòu)SDXL負(fù)責(zé)把畫面質(zhì)量拉起來。問題在于這套組合會成倍放大推理耗時。我自己的項目里一個中期的漫劇劇本大約會拆出300到500個分鏡。每個分鏡為了質(zhì)量篩選至少生成2到4張候選圖。這意味著一個項目就要跑上千次SDXL推理。如果單張卡的出圖速度是7秒一張不考慮排隊和錯誤重試光底圖生成就是2個小時起步的純GPU時間。在多人協(xié)作、頻繁改稿的節(jié)奏下這個成本很難接受。所以推理加速不是“錦上添花”而是能不能批量交付的關(guān)鍵。1.2 SDXL推理耗時拆解UNet采樣循環(huán)是絕對大頭要把加速做明白先得知道時間都花在哪。SDXL一次完整推理可以切成三段兩個文本編碼器各跑一次、UNet擴散采樣循環(huán)迭代20到50步、VAE解碼器最后把latent還原成像素圖。我實測中文本編碼加VAE通常只占0.6到0.9秒剩下幾乎全部時間都壓在UNet的采樣循環(huán)里。以RTX 4090、fp16、1024x1024分辨率、20步采樣為例原始diffusers管線單張總耗時在8秒左右UNet采樣循環(huán)至少占7秒。換算下來每一步UNet forward平均要跑350到400毫秒。而這一步的前向過程在fp16下會啟動大概幾百個CUDA kernel。每個kernel本身很快有的只有幾十微秒但幾百個kernel一個接一個從CPU提交到GPU調(diào)度和等待開銷就攢成了幾十毫秒甚至上百毫秒的“隱形浪費”。這也是為什么單純看計算量會覺得SDXL不應(yīng)該這么慢但實際跑起來就是慢。GPU卡在CPU逐個下發(fā)kernel的節(jié)奏上計算單元一直處于“吃不滿”的狀態(tài)。想解決無非兩條路把kernel合并掉或者把kernel的下發(fā)成本壓下去。CUDA Graph走的是后面這條路。1.3 對比一圈后為什么選CUDA Graph在決定用CUDA Graph之前我把常見的加速手段都過了一遍。TensorRT的加速效果確實最強但需要導(dǎo)出ONNX、構(gòu)建engine還要盯著算子兼容性SDXL的UNet結(jié)構(gòu)又復(fù)雜每次diffusers版本一升級engine基本要重新折騰一輪。torch.compile在部分模型上能拿到不錯的收益但它和PyTorch的版本、模式耦合很深遇到自定義attention反而容易踩坑。xFormers能省顯存也稍快一些但主要優(yōu)化的是attention算子解決不了CPU調(diào)度開銷的問題。CUDA Graph的思路完全不同它不碰你的模型權(quán)重也不改任何算子實現(xiàn)只把“CPU逐個啟動kernel”的模式改成“一次啟動整張kernel圖”。SDXL這種固定分辨率、固定batch、循環(huán)幾十步執(zhí)行同一個UNet的場景正好是CUDA Graph最能發(fā)揮價值的地方。低侵入、可回滾、對版本不敏感這是我選它的核心理由。2. CUDA Graph捕獲與重放到底加速在哪里2.1 被忽視的GPU空轉(zhuǎn)CPU逐個啟動kernel的開銷要理解CUDA Graph得先清楚GPU kernel的啟動鏈路。CPU端調(diào)用一個CUDA kernel時不是直接把函數(shù)扔給GPU執(zhí)行而是把kernel參數(shù)、啟動配置填進(jìn)命令緩沖區(qū)通過驅(qū)動提交到GPU前端。這個過程本身有固定開銷大概幾微秒到幾十微秒具體取決于驅(qū)動狀態(tài)和硬件平臺。單個kernel看起來不多但模型一復(fù)雜就完全不一樣了。我拿UNet一個step的kernel數(shù)量估算過在fp16推理、無attention特殊優(yōu)化的情況下一次forward會產(chǎn)生幾百個kernel。如果每個kernel的CPU啟動開銷平均按10到20微秒算一個step僅調(diào)度開銷就可能累積到幾毫秒到十幾毫秒。更麻煩的是CPU和GPU之間天然存在異步執(zhí)行CPU提交完一個kernel后可能已經(jīng)跑到下一個kernel的提交邏輯里但GPU必須等前面的kernel執(zhí)行完才能開始下一個這中間如果CPU提交速度跟不上GPU就出現(xiàn)空閑氣泡。打個比方這就像點外賣CPU是那個接單的人GPU是送餐員。正常模式是一次只接一單送完再回來接下一單路上來回折騰送餐員大部分時間都在空跑。CUDA Graph相當(dāng)于提前把一整天的配送路線全部規(guī)劃好接單員一次性把幾百單的路線交給送餐員他照著路線跑就行。省掉的不是送餐時間而是接單、分單、來回溝通的中間損耗。2.2 捕獲、實例化、重放三步走圖和重放各負(fù)責(zé)什么CUDA Graph的生命周期可以分成三個階段。第一個階段是捕獲也就是在啟動“記錄”的CUDA stream上正常執(zhí)行計算任務(wù)驅(qū)動會把所有kernel的啟動順序、參數(shù)、依賴關(guān)系原樣記錄下來。這個階段不能有CPU和GPU的同步操作比如item()、synchronize()否則捕獲會直接在運行時拋錯。第二個階段是實例化驅(qū)動把捕獲到的圖結(jié)構(gòu)編譯成一份可執(zhí)行的cudaGraphExec_t并且在這個階段完成內(nèi)存地址的靜態(tài)綁定和依賴優(yōu)化。第三個階段是重放調(diào)用一次cudaGraphLaunchGPU按圖里固化好的順序批量執(zhí)行所有kernel。PyTorch對這套流程做了封裝。torch.cuda.CUDAGraph()負(fù)責(zé)創(chuàng)建一個圖對象torch.cuda.graph(graph)上下文管理器負(fù)責(zé)控制捕獲范圍之后每次調(diào)用graph.replay()就會重放一次。重放時你不需要再逐個跑UNet里的kernelCPU只要發(fā)起一次圖啟動剩下的全交給GPU。需要特別強調(diào)的是“圖”記錄的不是PyTorch的autograd計算圖而是GPU kernel級的有向無環(huán)圖。它不關(guān)心你們的張量從哪里來也不關(guān)心反向傳播只關(guān)心kernel怎么按順序跑。所以在推理場景用CUDA Graph和訓(xùn)練時的動態(tài)圖完全是兩回事。2.3 為什么固定shape是硬約束而SDXL采樣恰好滿足捕獲的時候每個kernel的grid尺寸、block尺寸、輸入輸出張量的顯存地址都會被固化到圖里。這意味著重放時輸入數(shù)據(jù)的shape和dtype必須和捕獲時完全一致。如果下一次輸入的latent從1張變成2張kernel啟動配置里的block數(shù)就對不上了輕則結(jié)果錯誤重則顯存越界直接崩掉。這是CUDA Graph最核心的約束。SDXL的擴散采樣循環(huán)為什么非常適合因為去噪的每一步都在做同一件事把當(dāng)前l(fā)atent、timestep、文本條件輸入UNet輸出預(yù)測的噪聲殘差。只要分辨率固定latent的shape永遠(yuǎn)是(batch, 4, H/8, W/8)文本embedding的seq_len也固定整個計算圖的形狀不會變。有人會問timestep不是從1000一路變到0嗎注意timestep雖然數(shù)值變了但它的shape是固定的比如(1,)或者(batch,)。CUDA Graph固化的只是kernel結(jié)構(gòu)和顯存地址并不要求輸入數(shù)值不能變。你在重放前往靜態(tài)緩沖區(qū)里copy一個新的timestep值圖里的kernel會讀到新數(shù)值行為完全正常。這就是“動態(tài)值”和“動態(tài)shape”的區(qū)別也是CUDA Graph能在diffusion模型上落地的前提。3. SDXL推理接入CUDA Graph的實操方案3.1 先手動接管采樣循環(huán)別硬套pipeline直接用diffusers的StableDiffusionXLPipeline.__call__去接CUDA Graph是不現(xiàn)實的因為pipeline內(nèi)部有不少Python邏輯tokenize、文本長度截斷、CFG分支、scheduler.step、后處理這些都會破壞捕獲過程。正確做法是把采樣循環(huán)拆出來自己控制每一步的UNet調(diào)用。我習(xí)慣只用diffusers做模型加載采樣循環(huán)自己寫。這樣做的好處是控制力強既能方便插入捕獲邏輯也能在調(diào)度器、CFG策略上靈活調(diào)整。下面是我平時搭的骨架import torch from diffusers import DiffusionPipeline pipe DiffusionPipeline.from_pretrained( stabilityai/stable-diffusion-xl-base-1.0, torch_dtypetorch.float16, variantfp16, use_safetensorsTrue ).to(cuda) unet pipe.unet unet.eval() class UNetWrapper(torch.nn.Module): def __init__(self, unet): super().__init__() self.unet unet def forward( self, latent, timestep, encoder_hidden_states, text_embeds, time_ids, ): out self.unet( latent, timestep, encoder_hidden_statesencoder_hidden_states, added_cond_kwargs{ text_embeds: text_embeds, time_ids: time_ids, }, ) return out.sample unet_wrapper UNetWrapper(unet)這層wrapper的目的不是加功能而是把UNet forward的入?yún)⑹諗砍晒潭ㄔM方便后面統(tǒng)一往靜態(tài)緩沖區(qū)里copy。實際使用中文本編碼和VAE解碼放在圖外只有UNet采樣循環(huán)進(jìn)入CUDA Graph。3.2 靜態(tài)輸入輸出緩沖區(qū)怎么設(shè)計捕獲前必須給所有輸入輸出分配好靜態(tài)緩沖區(qū)。這些張量一旦分配在整個生命周期內(nèi)不能釋放、不能改變shape重放時所有數(shù)據(jù)都通過copy_操作進(jìn)入這些緩沖區(qū)。SDXL的UNet主要輸入我列成了下面這張表實際項目里按自己的模型配置調(diào)整即可張量Shapedtype說明latent(1, 4, 128, 128)fp16當(dāng)前去噪latent1024x1024分辨率對應(yīng)128x128timestep(1,)fp32當(dāng)前時間步數(shù)值會變但shape不變encoder_hidden_states(1, 77, 2048)fp16兩個文本編碼器拼接后的條件embeddingtext_embeds(1, 1280)fp16SDXL的pooled文本embedding走added_cond_kwargstime_ids(1, 6)fp32原始尺寸、裁剪坐標(biāo)、目標(biāo)尺寸等6個數(shù)值output(1, 4, 128, 128)fp16UNet輸出噪聲預(yù)測需要提醒的是diffusers內(nèi)部對timestep的處理有時會傳torch.long但UNet的時間編碼層會做float轉(zhuǎn)換。我自己在捕獲時統(tǒng)一用fp32的timestep避免dtype不一致導(dǎo)致重放結(jié)果異常。text_embeds和encoder_hidden_states在SDXL里必須是fp16這個由模型權(quán)重dtype決定別用fp32去copy。靜態(tài)緩沖區(qū)按照上面的表統(tǒng)一創(chuàng)建用torch.empty就行不用初始化成零。捕獲開始前往這些緩沖區(qū)里塞一批真實shape的數(shù)據(jù)跑幾次forward讓cuBLAS和cuDNN完成autotune并分配好workspace否則捕獲出來的圖可能落回保守算法加速效果打折扣。3.3 完整捕獲與重放代碼以及每行的意圖捕獲的核心流程是warmup、進(jìn)入torch.cuda.graph、執(zhí)行一次完整UNet forward。下面這段代碼是我在項目里跑通的版本做了注釋方便對照# 預(yù)熱這里跑3次觸發(fā)底層庫的算法選擇 for _ in range(3): _ unet_wrapper( s_latent, s_timestep, s_hidden, s_text_embeds, s_time_ids, ) torch.cuda.synchronize() # 捕獲 graph torch.cuda.CUDAGraph() with torch.cuda.graph(graph): s_output.copy_( unet_wrapper( s_latent, s_timestep, s_hidden, s_text_embeds, s_time_ids, ) ) torch.cuda.synchronize()預(yù)熱為什么要跑3次底層庫會在第一次調(diào)用時做autotune第二次、第三次基本會命中緩存里的最優(yōu)配置。如果跳過預(yù)熱直接捕獲捕獲時偶爾會觸發(fā)一些一次性分配或慢路徑選擇導(dǎo)致圖里的kernel不是最優(yōu)狀態(tài)。捕獲時我在圖內(nèi)做了一次copy_把UNet的輸出寫入預(yù)先分配的s_output緩沖區(qū)。這樣做的原因是輸出張量必須有一個固定地址后續(xù)每次重放都會往這個地址寫結(jié)果。捕獲完成后每次采樣step就只需要做兩件事把當(dāng)前輸入copy進(jìn)靜態(tài)緩沖區(qū)然后調(diào)用graph.replay()。def run_unet_step(latent, timestep, hidden, text_embeds, time_ids): s_latent.copy_(latent, non_blockingTrue) s_timestep.copy_(timestep, non_blockingTrue) s_hidden.copy_(hidden, non_blockingTrue) s_text_embeds.copy_(text_embeds, non_blockingTrue) s_time_ids.copy_(time_ids, non_blockingTrue) graph.replay() return s_output.clone()copy_和replay()都在當(dāng)前CUDA stream上執(zhí)行順序是有保障的。源張量和靜態(tài)緩沖區(qū)都在顯存里non_blockingTrue可以避免不必要的同步等待。返回時用clone()復(fù)制一份是為了防止下一次step重放把這塊顯存里的結(jié)果覆蓋掉。如果你不clone后面scheduler拿到的是同一個地址下一輪會被改寫結(jié)果亂掉。3.4 配合xFormers、ControlNet和CFG批量的一攬子策略CUDA Graph不是孤立用的實際工程里往往疊加其他優(yōu)化。我先說xFormers和SDPA?,F(xiàn)在diffusers新版本里attention默認(rèn)走torch.nn.functional.scaled_dot_product_attention在Ampere以上架構(gòu)本身就有加速效果。再套CUDA Graph時SDPA的kernel同樣會被捕獲進(jìn)圖里二者疊加沒有沖突。如果你的環(huán)境用了xFormers的MemoryEfficientAttention也是一樣的只要注意力輸入shape固定捕獲照常進(jìn)行。ControlNet是漫劇工作流里的???。ControlNet不會改變UNet的計算圖只是額外增加一組條件和中間特征。實際操作時我建議把ControlNet的forward也放進(jìn)同一個捕獲范圍內(nèi)讓它的kernel同樣被圖固化。這樣可以避免ControlNet帶來的額外kernel啟動次數(shù)抵消UNet本身的優(yōu)化收益。CFG批量值得專門提一句。做Classifier-Free Guidance時每個step要同時跑條件分支和無條件分支常規(guī)做法是把兩個輸入拼成一個batch讓UNet一次推理處理兩份數(shù)據(jù)。捕獲時直接把batch維度設(shè)為2靜態(tài)緩沖區(qū)的latent shape定為(2, 4, 128, 128)這樣一次replay就完成了兩個分支的計算。CUDA Graph對batch維度是敏感的捕獲時是2重放時就不能換回1所以如果需要靈活切換就為batch1和batch2各捕獲一張圖運行時按需選擇。4. 實測加速比、顯存開銷與參數(shù)調(diào)優(yōu)4.1 單張4090上的實測數(shù)據(jù)我在自己的測試環(huán)境里跑過幾組對比硬件是RTX 4090 24GBPyTorch 2.1diffusers 0.2x模型是SDXL base 1.0分辨率1024x1024采樣器Euler20步。每張圖測5次取平均單張推理包括文本編碼、UNet采樣循環(huán)和VAE解碼全流程結(jié)果如下表配置單張耗時相對原版提升原版diffusers fp168.2秒基線開啟xFormers/SDPA7.3秒約11%只加CUDA Graph6.1秒約26%SDPA CUDA Graph5.4秒約34%可以看到CUDA Graph單獨帶來的收益在20%以上和attention優(yōu)化疊加后能到三成多。換算到UNet單步耗時從約370毫秒降到了約245毫秒。這一步省下來的時間主要就是CPU調(diào)度和GPU空轉(zhuǎn)的損耗。不同顯卡上收益會有差異。顯卡越弱、kernel越短CUDA Graph的收益越明顯因為調(diào)度開銷在單步耗時里的占比更大。反過來如果用H100這種計算極強的卡kernel本身更快啟動開銷占比反而可能更高收益也不小??傊@種加速手段不吃模型、不吃顯卡架構(gòu)只要CUDA版本支持基本都能拿到穩(wěn)定回報。4.2 CUDA Graph的顯存代價以及內(nèi)存池復(fù)用捕獲會帶來一個容易被忽略的問題顯存占用上升。普通推理時中間激活張量用完就釋放同一塊顯存可以反復(fù)被不同tensor使用。但CUDA Graph捕獲時圖里所有中間tensor都必須固定在特定顯存地址上不能再被復(fù)用。SDXL的UNet一次forward會產(chǎn)生不少大激活張量全部固化下來可能額外占掉1到2GB顯存這個開銷在24GB卡上還能接受在8GB、12GB卡上就很緊張了。解決思路是復(fù)用內(nèi)存池。torch.cuda.CUDAGraph在捕獲時會綁定一個memory pool后續(xù)捕獲如果重復(fù)申請新pool顯存碎片會非常難看。我在代碼里是這樣處理的第一次捕獲后保存pool graph.pool()第二次捕獲時把pool傳進(jìn)去with torch.cuda.graph(graph2, poolpool):這樣兩個圖共享同一塊池子顯存不會翻倍增長。如果你的顯存實在緊張還可以把VAE解碼和文本編碼都留在圖外只捕獲UNet采樣循環(huán)因為UNet才是激活值的大戶。我在漫劇流水線上就是這么干的既能保住加速收益又能控制顯存水位。4.3 batchsize和分辨率變化時的收益曲線我還專門測過batchsize對收益的影響。batch1時CPU調(diào)度開銷在單步耗時里的占比最高CUDA Graph收益最明顯能到25%以上。隨著batch變大比如batch4kernel每個都更“胖”GPU單次kernel運行時間變長CPU啟動開銷占比自然下降CUDA Graph的收益會回落到10%到15%左右。漫劇分鏡場景絕大多數(shù)時候是batch1或batch2正好落在收益最高的區(qū)間里。分辨率的影響也類似。1024x1024換成768x768時latent縮小到96x96kernel運行時間變短調(diào)度開銷占比變大CUDA Graph的加速比會更高。反過來換成1536x1536kernel運行時間拉長收益比例會稍微下降但由于整體耗時變長省下的絕對時間依然可觀。這里要提醒的硬約束是CUDA Graph和分辨率強綁定。你捕獲了一個1024x1024的圖就不能用在768x768的輸入上因為latent的shape變了grid配置完全對不上。漫劇項目如果同時存在豎版、橫版、方圖等多種規(guī)格我的做法是給每種常用規(guī)格單獨捕獲一張圖運行時按輸入shape分發(fā)。5. 實測中遇到的坑與排查思路匯總5.1 捕獲失敗的常見原因與處理捕獲階段最容易碰到的是cudaStreamCapture相關(guān)報錯。我踩過最多的坑是沒預(yù)熱就捕獲cuBLAS的workspace或算法選擇還沒穩(wěn)定捕獲過程中觸發(fā)了某些不允許的行為。解決辦法就是老老實實在捕獲前跑2到3次warmup forward并且這幾次warmup的shape和后續(xù)完全一致。第二個常見坑是捕獲里面混入了CPU同步操作。有一次我在代碼里為了調(diào)試打印了一個中間張量的shapeprint(s_latent.shape)其實不會同步但如果我寫成print(s_latent.sum().item())就會觸發(fā)GPU到CPU的同步捕獲直接失敗。排查技巧是把捕獲段里的代碼收斂到最干凈任何Python側(cè)判斷、同步、item()都移到捕獲外面。第三個坑是作用域內(nèi)變量被重新賦值。捕獲圖記錄的是顯存地址不是變量名。如果你在with torch.cuda.graph(g)塊里寫了output unet(...)塊外面又把output指向另一個張量圖本身不會有問題但后續(xù)你可能誤讀了錯誤的指針結(jié)果一跑就錯。統(tǒng)一用預(yù)先分配的buffer不用臨時變量名能規(guī)避掉這類問題。5.2 重放結(jié)果不對的罪魁禍?zhǔn)字胤艌?zhí)行完圖是“成功”運行了但出圖結(jié)果明顯不對比如畫面有噪塊、顏色整體偏掉或者同一張圖兩次重放結(jié)果不同。這類問題大多數(shù)出在輸入沒有完整更新上。我遇到過一次比較隱蔽的情況所有輸入都copy了唯獨time_ids漏了但SDXL對time_ids的敏感度沒有l(wèi)atent那么高短步數(shù)下肉眼不太容易看出來長步數(shù)一跑就露餡。排查時把五個靜態(tài)輸入做一個版本校驗每個step記錄一次是否更新能在幾分鐘內(nèi)定位。還有一個更陰間的坑copy_是異步操作如果源張量來自另一個CUDA stream且那個stream沒有和當(dāng)前stream做同步重放時靜態(tài)緩沖區(qū)可能只copy了一半圖里讀到的是臟數(shù)據(jù)。做多stream并行時必須在copy之前調(diào)用src_stream.wait_stream(dst_stream)之類的事件同步確保數(shù)據(jù)落地再replay。輸出側(cè)的問題也有。我早期圖省事不clone輸出直接返回s_output結(jié)果scheduler里對輸出做原地更新把靜態(tài)緩沖區(qū)污染了下一輪重放輸入都被改成上一次的輸出。教訓(xùn)是圖內(nèi)使用靜態(tài)buffer圖外必須把結(jié)果當(dāng)成“一次性讀取”的數(shù)據(jù)處理要么clone要么盡快消費。5.3 與其他加速手段共存時的沖突排查torch.compile和CUDA Graph放一起用是我踩過最久的坑。PyTorch的torch.compile在某些后端里自己就會生成CUDA Graph你再去手動捕獲等于把一個圖啟動的kernel序列又包了一層圖輕則收益不明顯重則捕獲失敗。經(jīng)驗是二選一優(yōu)先保留手動CUDA Graph因為它的執(zhí)行路徑更可控排查起來也更簡單。TensorRT engine和CUDA Graph能不能共存能但要小心顯存爭搶。TensorRT的engine會保存自己的workspaceCUDA Graph的pool也會占一塊顯存兩者加起來可能把顯存壓到危險水位。我的處理方式是把CUDA Graph的pool顯存上限控制住盡量避免同時把多個分辨率的圖全部裝進(jìn)顯存用的時候再切。多卡環(huán)境還有個小陷阱CUDA Graph是和設(shè)備綁定的。你在0號卡上捕獲的圖不能直接拿到1號卡上去replay即使兩臺卡型號完全一樣也不行。多卡推理要么每張卡各自捕獲要么用同樣的初始化流程每進(jìn)程捕獲一次千萬不要想“copy一下圖對象就完事”。最后說一個和采樣器兼容性相關(guān)的問題。DDIM、Euler、DPM這類調(diào)度器它們的scheduler.step()里有大量Python側(cè)的計算不適合放進(jìn)CUDA Graph。一定要堅持只捕獲UNet forward讓調(diào)度器在圖外運行。這個邊界理清楚之后換采樣器、換步數(shù)都只是改調(diào)度器參數(shù)的事CUDA Graph部分完全不用動。我個人在漫劇批量出圖流水線上把CUDA Graph加進(jìn)去之后最大的感受不是單張圖快了多少而是GPU的利用率肉眼可見地上去了。以前每張圖的間隔里總有那么幾十毫秒在空等CPU調(diào)度批量任務(wù)一多這種空等會疊成幾分鐘的額外耗時。CUDA Graph相當(dāng)于把“每頓外賣只送一單”改成“提前把一天的路線規(guī)劃好一次跑完”聽起來玄實際操作起來就是個兩三天能完成的小工程。如果你也在用SDXL批量出圖并且卡在推理延遲上我的建議是先把這個做完再考慮換卡、加機器這些花大錢的路子。