練加速實(shí)踐)
訓(xùn)練腳本剛跑起來終端就飄過一行WARNING:tensorflow: Not using XLA:CPU for cluster很多人的第一反應(yīng)是“我的分布式集群是不是壞了”“CPU 是不是不支持 XLA”。其實(shí)這是 TensorFlow 在編譯優(yōu)化階段打出來的一條典型 warning意思是你圖中有一部分計算子圖cluster本來可以在 XLA:CPU 設(shè)備上編譯執(zhí)行但最終沒有被 XLA 接管退回到了默認(rèn)的普通算子執(zhí)行路徑。這不是 error程序大概率還會正常跑。但如果你跑的是訓(xùn)練任務(wù)尤其是大規(guī)模卷積、Transformer、圖像生成這類計算密集模型這行 warning 很值得較真一下它往往意味著你白等了。這篇內(nèi)容我會從 XLA 的 cluster 到底是什么講起給出我實(shí)際排查用的命令和配置再聊幾個“日志消失但速度反而變差”的隱藏坑希望能幫你從“消警告”走向“真提速”。1. 先拆開這句話XLA、CPU 與 cluster 分別指什么1.1 cluster 不是分布式集群而是計算子圖第一次看到“cluster”這個詞很容易聯(lián)想到分布式訓(xùn)練里那套節(jié)點(diǎn)集群。但在這里它指的是計算圖里的一批連續(xù)算子。TensorFlow 訓(xùn)練時會把模型表達(dá)成一張有向計算圖。XLA 這個編譯器不會一個一個算子去優(yōu)化而是把圖中可以合并的算子切成一個一個小組每個小組就是一個 cluster。它就像把好幾個菜打包成一份“套餐”交給同一個廚師一次做完而不是每個菜都重新開一次火。所以Not using XLA:CPU for cluster的意思是這份“套餐”沒有交給 XLA 大廚而是回到了默認(rèn)流程按老辦法一個算子一個算子運(yùn)行。1.2 為什么訓(xùn)練過程中會冒出來這個 warning 最常出現(xiàn)在兩種場景你打開了 XLA 自動聚類autoclusteringGrappler 優(yōu)化器掃描計算圖后發(fā)現(xiàn)某個 cluster 可以做 XLA 編譯但運(yùn)行時環(huán)境不滿足條件你用了tf.function(jit_compileTrue)或者tf.xla.experimental.jit_scope()這種顯式開啟方式但某個片段沒有成功走 XLA 執(zhí)行路徑。TensorFlow 在決定“一個 cluster 是否要用 XLA:CPU”時需要同時檢查很多東西XLA 設(shè)備是否注冊、cluster 里的算子是否都有對應(yīng) XLA kernel、shape 是否靜態(tài)可推斷、是否有循環(huán)或控制流依賴等。任何一個條件不滿足它就會放棄這個 cluster并打出這么一行 warning。1.3 它和不報錯是兩件事很多人會誤以為“只是 warning不用管”。確實(shí)如果你只是做個幾十步的小 demo那無所謂。但如果訓(xùn)練一個要跑十幾個小時的模型XLA 的融合優(yōu)化有時候能帶來 20%~50% 的端到端收益這時候每一行 warning 都可能是性能損失點(diǎn)。更關(guān)鍵的是這個 warning 經(jīng)常把真正的問題藏起來。真正導(dǎo)致 cluster 無法編譯的原因并不會默認(rèn)出現(xiàn)在終端而是被 TF 自己的日志級別過濾掉了。所以看到它第一件事不是找一堆環(huán)境變量亂試而是搞清楚到底是設(shè)備沒注冊、聚類沒開、還是某個 cluster 本身不滿足編譯條件。2. 定位根因日志背后的三層開關(guān)我在處理這類問題時習(xí)慣按三層來排查設(shè)備注冊、自動聚類、單個 cluster 的可編譯性。2.1 第一層XLA 設(shè)備有沒有注冊XLA:CPU 雖然名字里帶 CPU但它并不是一個真實(shí)物理設(shè)備而是邏輯上的編譯后端。TF 里有一類虛擬設(shè)備叫XLA_CPU、XLA_GPU用來承接 XLA 編譯后的 executable。如果沒有注冊這類設(shè)備那訓(xùn)練時根本沒地方“運(yùn)行”編譯好的 XLA kernel自然就會出現(xiàn) warning。先跑一段代碼確認(rèn)一下import tensorflow as tf print(physical devices:, tf.config.list_physical_devices()) print(logical devices:, tf.config.list_logical_devices())正常情況下你會看到類似輸出Physical devices: - CPU - GPU (如果有機(jī)型卡) Logical devices: - CPU:0 - GPU:0 - XLA_CPU:0 - XLA_GPU:0如果Logical devices里沒有XLA_CPU:0那基本可以斷定設(shè)備注冊這一步就卡住了。老版本 TF 2.4~2.7 里XLA 設(shè)備默認(rèn)是隱藏的需要通過環(huán)境變量把它暴露出來這個我后面會給出具體寫法。2.2 第二層Grappler 自動聚類有沒有開即使設(shè)備注冊了XLA 也不會默認(rèn)接管所有計算。TensorFlow 里有一個叫 Grappler 的優(yōu)化器它負(fù)責(zé)掃描tf.function對應(yīng)的圖并把能編譯的算子合并成 cluster。合并動作就叫“自動聚類”。打開自動聚類的方式很簡單import tensorflow as tf tf.config.optimizer.set_jit(True) # 或 tf.config.optimizer.set_jit(autoclustering)這行代碼要放在模型構(gòu)建和訓(xùn)練步驟定義之前。你可以把它理解為對 Grappler 說“看到合適的 cluster就去嘗試 XLA 編譯?!比绻阋呀?jīng)開了自動聚類設(shè)備列表里也有 XLA_CPU但還是看到 warning那問題通常出在第三層某個 cluster 本身編譯不了。2.3 第三層具體哪個 cluster 被拒默認(rèn)情況下 TensorFlow 的日志級別比較高很多關(guān)鍵信息被壓掉了。我會先把日志級別調(diào)到最安靜但保留 debug 信息export TF_CPP_MIN_LOG_LEVEL0 export TF_CPP_MIN_VLOG_LEVEL1 python train.py 21 | grep -E XLA|cluster|Not usingTF_CPP_MIN_VLOG_LEVEL1會讓底層輸出大量調(diào)試日志跑起來會刷屏所以最好配合 grep 看。這時候你可能會看到更具體的描述比如cluster requires static shapescluster has unsupported opEnable XLA to get more accurate …這些才是根因。我遇到過不少情況是tf.shape使用了動態(tài)維度或者某個自定義 op 沒有注冊 XLA kernel導(dǎo)致整個 cluster 被整體放棄。如果你一味地去加環(huán)境變量而不去看這一層的日志永遠(yuǎn)只是在表面打轉(zhuǎn)。3. 我的實(shí)測路徑從警告到正常執(zhí)行的四種做法下面我按從傳統(tǒng)到現(xiàn)代的路徑給出四種實(shí)際可用的配置方式。它們不是互斥的但適用的 TensorFlow 版本和場景不同。3.1 老版本方案用環(huán)境變量注冊 XLA 設(shè)備在 TF 2.4 到 2.7 附近XLA 設(shè)備默認(rèn)是隱藏的。常見做法是在啟動訓(xùn)練前加一個環(huán)境變量TF_XLA_FLAGS--tf_xla_enable_xla_devices python train.py這個 flag 的作用就是讓XLA_CPU、XLA_GPU這類邏輯設(shè)備出現(xiàn)在運(yùn)行時環(huán)境中讓后面的 XLA cluster 編譯結(jié)果有地方可放。如果你用的是 Docker記得在容器啟動時傳進(jìn)去docker run -e TF_XLA_FLAGS--tf_xla_enable_xla_devices ...在代碼里提前設(shè)置也可以但必須在 import TensorFlow 之前import os os.environ[TF_XLA_FLAGS] --tf_xla_enable_xla_devices import tensorflow as tf注意新版 TensorFlow2.10 之后已經(jīng)默認(rèn)注冊 XLA 設(shè)備這個 flag 不一定再起作用。所以當(dāng)你在新版本里發(fā)現(xiàn)這個參數(shù)沒效果不要繼續(xù)糾結(jié)直接看下一種做法。3.2 現(xiàn)代主流做法打開自動聚類從 TF 2.8 開始我首選tf.config.optimizer.set_jit。它可以接受布爾值和字符串import tensorflow as tf tf.config.optimizer.set_jit(True) # 或 tf.config.optimizer.set_jit(autoclustering) # 之后正常定義模型 model tf.keras.Sequential([...]) model.compile(...)這樣設(shè)置后Grappler 會對tf.function里的連續(xù)算子做聚類嘗試。如果某個 cluster 成功你會看到執(zhí)行日志里出現(xiàn)XlaLaunch算子如果失敗控制臺會繼續(xù)出現(xiàn)Not using XLA:CPU for cluster但此時你可以參考上一節(jié)的方法進(jìn)一步定位具體 cluster 失敗原因。這種方式的優(yōu)點(diǎn)是省心適合已有訓(xùn)練腳本、不想大改代碼的情況。缺點(diǎn)是你無法精確控制哪些 cluster 編譯、哪些不編譯有時候小算子也會被打包反而帶來額外開銷。3.3 顯式強(qiáng)約束jit_compileTrue如果你希望“這段計算必須走 XLA”那就不要再用隱式 warning 來猜了直接給tf.function加約束tf.function(jit_compileTrue) def train_step(x, y): with tf.GradientTape() as tape: pred model(x, trainingTrue) loss loss_fn(y, pred) grads tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables)) return lossjit_compileTrue會強(qiáng)制 TensorFlow 把這個函數(shù)中的可編譯部分交給 XLA 編譯。如果編譯失敗它通常不會靜默退到普通路徑而是直接拋錯或者打出更加明確的編譯錯誤。這非常適合用來“逼問”到底哪里不支持 XLA。還有一個更老的 APIwith tf.xla.experimental.jit_scope(): result my_compute(x, y)它適合局部控制某一段代碼但優(yōu)先級和表達(dá)能力都不如jit_compileTrue老項(xiàng)目中遇到可以認(rèn)識一下新項(xiàng)目我不太推薦。提示jit_compileTrue不等于所有代碼都能被 XLA 編譯。遇到while_loop、動態(tài) shape、某些自定義 TF op 時仍然可能失敗。它最大的價值是讓你更快看到失敗原因而不是保證一定成功。3.4 組合拳老腳本怎么低成本接入如果是維護(hù)一個半年沒動過的訓(xùn)練腳本我會建議這樣一步步來不要一下全改先加環(huán)境變量TF_XLA_FLAGS--tf_xla_enable_xla_devices保證設(shè)備注冊再在訓(xùn)練腳本頂部加tf.config.optimizer.set_jit(True)如果還看到 warning挑最耗時的訓(xùn)練函數(shù)臨時加jit_compileTrue根據(jù)編譯錯誤逐個處理比如把動態(tài) sequence 做 padding把不支持的 op 拆到j(luò)it_compile函數(shù)外面。這套組合我實(shí)測下來大部分“Not using XLA:CPU for cluster”都能在半小時內(nèi)定位到原因。日志確實(shí)會消失但真正的檢查才剛剛開始。4. 日志消失不等于提速三個容易誤判的細(xì)節(jié)很多時候我們把 warning 搞沒了就收工結(jié)果整體訓(xùn)練時間一點(diǎn)沒少。這里有幾個非常隱蔽的坑值得單獨(dú)說。4.1 動態(tài) shape 導(dǎo)致反復(fù)重編譯XLA 是一個靜態(tài)編譯編譯器它在編譯時會固定輸入 tensor 的 shape。你的模型如果 batch size 固定、輸入圖片尺寸固定那編譯一次可以反復(fù)用。但如果你一邊訓(xùn)練一邊動態(tài)改變輸入長度比如 NLP 里常見的 variable sequence lengthXLA 每遇到一個新 shape 就可能重新編譯一次。重新編譯的時間往往比你省下的時間還要多最后你會看到 “日志沒了但每步 step 反而慢了不少”。我甚至見過 CPU 上因?yàn)轭l繁 recompile訓(xùn)練時間翻倍的案例。解決辦法有兩個方向在數(shù)據(jù) pipeline 里對序列做統(tǒng)一 padding把 shape 固定下來把變長部分拆到 XLA 編譯范圍之外只讓固定 shape 的核心矩陣運(yùn)算走 XLA。固定 shape 對于 XLA 來說幾乎是第一原則性能影響比編譯器版本大得多。4.2 不支持的算子會讓整個 cluster 全回退XLA:CPU 不是萬能的有些 TensorFlow 算子它沒有實(shí)現(xiàn) kernel比如部分字符串處理、某些高級索引操作、稀疏相關(guān)算子。如果 Grappler 把一個 cluster 里的幾十個算子全部包在一起只要里面有一個算子 XLA 不支持整個 cluster 就會回退之前所有合并努力全部白費(fèi)。這就是為什么有時候你只改了一小段代碼warning 突然消失又突然回來。問題很可能不是 XLA 本身而是你圖里的一個不起眼的小算子。排查時建議把模型拆成更小的tf.function并配合jit_compileTrue分段測試。哪一段報錯就說明問題在哪一段。然后要么把不支持的算子挪出該函數(shù)要么用 XLA 支持的等價操作重寫。4.3 XLA:CPU 和 oneDNN 不是一回事別混著用CPU 訓(xùn)練還有一個常見的加速通道叫 oneDNN以前叫 MKL-DNN。很多人看到兩個都是 CPU 方向就以為相關(guān)。但 oneDNN 優(yōu)化的是單個算子內(nèi)部的數(shù)學(xué)實(shí)現(xiàn)比如卷積、矩陣乘XLA 優(yōu)化的是整個 cluster 的算子融合和內(nèi)存規(guī)劃。兩者的關(guān)系是并行的不是替代關(guān)系。你單獨(dú)設(shè)置TF_ENABLE_ONEDNN_OPTS也不會消除Not using XLA:CPU for cluster因?yàn)檫@句話說的是 XLA 編譯器有沒有接管 cluster和 oneDNN 無關(guān)。如果你追求性價比可以兩個都打開。但注意同時開啟時 profile 結(jié)果可能比較難讀得區(qū)分XlaLaunch和 oneDNN 優(yōu)化算子分別占了多少時間。4.4 用 Profiler 驗(yàn)證而不是看 warning判斷 XLA 到底有沒有生效最可靠的方法不是看終端有沒有 warning而是看執(zhí)行期算子列表里有沒有XlaLaunch。我自己會這樣跑一輪import tensorflow as tf tf.profiler.experimental.start(/tmp/xla_profile) for step in range(step_num): train_step(data) tf.profiler.experimental.stop()然后打開 TensorBoardtensorboard --logdir/tmp/xla_profile在 Trace Viewer 里搜索XlaLaunch。如果你看到多個XlaLaunch算子出現(xiàn)說明 cluster 確實(shí)被 XLA 接管了如果完全沒有那即使 warning 被壓掉了也只是被“隱藏”不是被“解決”。這個檢查方式我越來越依賴因?yàn)樗苤苯臃从秤?xùn)練時間到底花在哪。只看 warning 的話很容易被表面現(xiàn)象誤導(dǎo)。5. 給不同版本 TF 的最終建議5.1 快速對照表不同 TensorFlow 版本對 XLA:CPU 的處理方式差異很大。我整理了一張表方便你按自己的版本快速對號入座場景推薦做法注意點(diǎn)TF 2.4 ~ 2.7設(shè)置TF_XLA_FLAGS--tf_xla_enable_xla_devices必須在 import TensorFlow 前設(shè)置TF 2.8 ~ 2.10使用tf.config.optimizer.set_jit(True)可以配合舊 flag 過渡TF 2.11優(yōu)先tf.config.optimizer.set_jit(autoclustering)XLA 設(shè)備默認(rèn)注冊特定計算塊使用tf.function(jit_compileTrue)編譯失敗時會快速暴露原因動態(tài) shape 模型padding 固定 shape動態(tài) shape 是 XLA 最大天敵訓(xùn)練沒提速使用 profiler 查XlaLaunch不要只看 warning5.2 我自己現(xiàn)在的“不折騰”組合我現(xiàn)在維護(hù)訓(xùn)練腳本時默認(rèn)會這樣做訓(xùn)練步整體包進(jìn)tf.function全局開tf.config.optimizer.set_jit(autoclustering)對最耗時的矩陣大塊單獨(dú)加一個jit_compileTrue的小函數(shù)所有可變長的輸入全部在數(shù)據(jù)管線里 padding 到固定 shape每改一次模型結(jié)構(gòu)用 profiler 看一遍XlaLaunch覆蓋范圍。這套組合犧牲了一點(diǎn)靈活性但勝在穩(wěn)定、可復(fù)現(xiàn)。至少我最近跑的幾個 CPU 訓(xùn)練任務(wù)里XLA 的收益能穩(wěn)定落在 10%~30% 之間而且不會再被“Not using XLA:CPU for cluster”這種提示反復(fù)干擾。老實(shí)說這一行 warning 本身不復(fù)雜復(fù)雜的是它背后牽扯的編譯機(jī)制、算子支持和 shape 策略。只要你不把“消 warning”當(dāng)最終目標(biāo)而是把“讓 XlaLaunch 真正跑起來、并且讓訓(xùn)練總時間降下去”當(dāng)目標(biāo)問題解決起來反而會順利得多。