執行圖
當你點擊 執行 時,前端會透過 WebSocket(ws://host/ws/execution)把圖送到後端,後端則在每個節點完成時把結果串流回來。
即時執行
- 後端會驗證圖(DAG 檢查、型別安全、至少一個
Start節點),對它進行拓撲排序(Kahn 演算法,含循環偵測),並平行執行彼此獨立的節點。 - 每個節點在執行過程中回報狀態:
running→completed(或error),並內嵌一份精簡的 輸出摘要 方便快速查看。 - 執行紀錄 分頁會顯示這種逐節點的進度,以及任何
Print節點的輸出。
沒有 trigger 的節點仍然可能執行
只移除節點的 trigger 邊,不會讓它退出這次執行。只要一條 data 邊仍把它的輸出連到會執行的節點,這個節點就會執行;連到必要輸入或選用輸入都沒有差別,也不要求它有自己的 trigger。Dataset 或轉換鏈的第一個節點通常沒有 trigger,並預期以這種方式執行。相同規則適用於其他已連線的節點。
實際效果是:只中斷 trigger 邊不再能停用分支。 若要保留節點的接線供稍後使用,但本次不執行它,請中斷其 data 邊;這才會將它從本次執行中移除。對於鏈中間的節點,可以使用略過這個節點(以右鍵點擊節點,或按 Ctrl/Cmd+B),跳過該節點並將輸入直接傳給原本的下游節點。這項功能只適用於有一個輸入型別與輸出相同、可直接轉送的節點。因此,完全沒有輸入的來源節點(CSVReader、ImageReader、Dataset 及其他檔案讀取節點)會拒絕這項操作。這類節點只能透過中斷 data 邊來停用。
這也會影響讀取節點。CSVReader、ImageReader 等節點只要仍連到一個輸入埠,即使是選用輸入埠,也會在沒有 trigger 時執行;過去這種情況會略過節點。如果節點指向的檔案已刪除或移動,先前能成功執行的圖可能會在該節點發生 FileNotFoundError。
訓練迴圈與 loss 圖表
TrainingLoop 節點會在訓練期間發出進度事件。結果面板的 訓練 分頁會在每個 epoch 完成時繪製 即時 loss 圖表,讓你即時觀察收斂情況。
部分重新執行(髒節點追蹤)
CodefyUI 會追蹤 dirty 節點。當你變更一個節點的參數或輸入時,只有該節點及其下游依賴會被標記為需要重新執行。未變更的節點會回傳快取輸出(顯示為 cached)。因此,調整單一超參數時只會重新執行圖中受影響的部分,可縮短開發期間的執行時間。
確定性的節點會自動快取;非確定性的節點(訓練迴圈、隨機運算,或任何 cacheable = False 的節點)則一律重新執行。
檔案讀取節點的內容感知快取
快取項目的 key 由節點類型、參數、上游節點的快取 key,以及執行裝置雜湊而成。節點從圖外部讀取的內容無法只由 params 表示:path 參數只記錄讀取位置,不包含該位置的內容。因此,讀取外部狀態的節點還會把內容指紋加入 key。指紋包含解析後檔案的大小與修改時間;對於不超過 8 MB 的檔案,還包含內容雜湊。即使同樣大小的修改發生在同一個檔案系統時間戳記刻度內,key 仍會改變。CSVReader、FileReader、ImageReader、ImageBatchReader、Dataset 和 ImageFolderDataset 都使用這項機制。編輯檔案後再次點擊 執行,會取得新內容;若檔案未變,則使用快取結果而不重新讀取。對於 Dataset 和 ImageFolderDataset,資料集中的任一檔案變更也會更新指紋。
Dataset 只會對該資料集所在的目錄建立指紋,例如 MNIST 的 MNIST/ 或 CIFAR-10 的 cifar-10-batches-py/,而不是整個 data_dir。在專案目錄中,每個資料集共用 assets/data/,旁邊還有 assets/models/。縮小指紋範圍後,儲存模型或下載另一個資料集不會再讓第一個資料集的快取失效並於下次執行時重新讀取。
GraphInput 搭配 type=image 進行畫布執行時,也使用相同機制。API 路徑已把呼叫者提供的值放在 params 中;畫布執行則會從磁碟載入 default 路徑。內容指紋可讓兩次畫布執行之間修改的圖片載入新像素。
從不快取的內容
有些節點會設定 cacheable = False,完全停用快取。原因分為四類:
指紋無法描述的外部狀態。 HuggingFaceDataset 和 KaggleDataset 會存取網路,KaggleDataset 還會使用 KAGGLE_* 環境憑證。本機快取目錄的指紋無法判斷遠端 revision 或憑證是否變更,因此這兩個節點每次執行都會重新取得資料。LLMChat 會存取遠端模型 API,也基於相同原因每次重新執行。
節點的目的本身就是副作用。 ImageWriter、ModelSaver 和 CheckpointSaver 用於寫入檔案。快取命中只會回傳已記錄的 {"path": ...},不會寫入磁碟;這不符合這些節點的用途。刪除輸出檔後重新執行,必須重新建立檔案。因此,不論上游輸入為何,這些節點每次都會重新執行。
TrainingLoop 也屬於這一類。它的主要產出不是回傳的 model 參考,而是該參考所指權重的變更,以及每個 epoch 的指標序列。快取命中無法重現這兩項結果,因此即使所有上游輸入都未變更,TrainingLoop 仍會在每次 run 重新執行。EvaluateModel 列入這一類的原因只有後半:準確率在它的輸出中,但它寫到準確率圖表的那個點不在,因此快取命中會讓回報成功的 run 留下空白圖表。
節點會回傳可變物件的即時參考。 model、optimizer 和學習率排程器都是訓練會就地修改的物件參考。快取 key 只描述物件建立時的架構、超參數及上游 key;第一個 batch 執行後,它就不再代表物件的目前狀態。重用已記錄的參考,會讓下一個 run 取得上一個 run 已訓練的物件。因此,ModelLoader、CheckpointLoader、Inference、Optimizer、LRScheduler、DQN、PPO 和 RewardModel 每次都會重新執行。
ModelLoader 和 CheckpointLoader 即使有內容指紋,仍屬於這一類。指紋描述它們讀取的內容,不描述它們寫入的內容:load_state_dict 會修改連接的 model,而快取命中會跳過這項操作。代價比聽起來小:停用快取會向下游傳播(見下文),而且只要它們的 model 輸入已連接,來源就是擁有權重的節點,那本來就不可快取,因此一般接線方式下這兩個節點原本就不會由快取提供結果。
節點可能有只有作者知道的副作用。 PythonScript 會執行畫布中輸入的程式碼。code 參數會納入快取 key,因此修改腳本後會重新執行;但腳本和輸入都未變更時會命中快取。腳本可能就地修改輸入 tensor 或 model、變更行程全域的 torch/numpy 狀態,或使用 ANY 型別輸入埠提供的任意物件。節點類型與原始碼檢查都無法完整描述這些行為,因此 PythonScript 一律停用快取。詳見 PythonScript 節點。
相同設定也適用於其他輸出無法由快取 key 完整描述的節點,包括 GaussianNoise、DDPMSampler、BackwardOnce、DiffusionTrainingLoop,以及所有擁有權重的節點(SequentialModel、DiffusionUNet 和每個 layer 節點,例如 Linear、Conv2d、LSTM)。這些節點的參數會隨訓練改變。
對於擁有權重的節點,設定 → 訓練行為 → 在多次執行間保留權重會決定後續 run 如何處理權重,不受輸出快取影響。這項設定預設開啟,因此下一個 run 會從前一次 run 的最終權重繼續。立即重置所有權重會捨棄儲存的權重,讓下一個 run 初始化新的模型。SequentialModel 每次 run 都會在執行紀錄中回報採取的動作。
停用快取會向下游傳播。任何接收這些節點輸出的節點也會重新執行,因為快取 key 只記錄上游節點的 key,不記錄實際輸出。若下游節點仍使用快取,它會回傳以舊資料計算的結果。
設定中的兩個開關會完全繞過快取。開啟顯示內部步驟或擷取梯度時,不會使用任何快取,所有節點都會重新執行。快取輸出不包含 __steps__ trace,也不包含保留梯度追蹤的 tensor,因此無法提供要求的內容。
預設組合(preset)與子圖方塊會回報什麼狀態
畫布上的 preset 節點和子圖實例各以一個方塊代表多個內部節點,因此只會回報一個整體狀態。狀態取決於內部節點實際執行的結果:
| 方塊顯示 | 意思 |
|---|---|
已完成 | 至少一個內部節點實際執行。即使其他節點使用快取,整體仍表示已執行工作。 |
已快取 | 所有內部節點都命中快取。這次沒有執行節點,顯示的輸出來自上一次 run。 |
已跳過 | 所有內部節點都因上游輸入失敗而略過。 |
錯誤 / 已中斷 | 內部節點失敗或提早停止。任一情況都會立即決定整個方塊的狀態。 |
已快取 狀態需要特別注意:包含 TrainingLoop 的 preset 回報 已快取 時,表示這次 run 沒有進行訓練。在區分這項狀態之前,兩種情況都顯示 已完成,無法從 preset 層級判斷變更是否讓內部節點重新執行。若要重新執行,請變更其依賴項目或清除快取。
可重現的執行(亂數種子)
預設情況下,run 使用 PyTorch 選擇的熵,因此同一張圖執行兩次時,權重初始化與洗牌順序會略有不同,loss 曲線也會不同。在設定 → 訓練行為中設定亂數種子,可讓 run 重現相同結果。
設定 seed 後:
- 每個節點都會使用由
(seed, node id)推導的值設定 seed。節點取得的亂數只取決於 seed 與自身 ID,不受圖中其他節點先前消耗的亂數量或引擎排程順序影響。 DataLoader會使用自己的 generator,因此每個 epoch 的洗牌順序固定,每個 worker 行程也有獨立的亂數串流。- run 一次只執行一個節點,而且不會與另一個 run 重疊。 設定 seed 會寫入行程全域的 RNG 狀態;如果第二個節點或第二個 run 同時從該狀態取值,就會改變數列。因此,seeded run 會先等待目前執行中的 run 結束,再單獨執行;之後送出的 run 也會等待它結束。未設定 seed 的 run 不受影響,彼此仍可並行,內部節點也仍可並行執行。
相同圖與相同 seed 在 CPU 上執行兩次時,會產生位元完全相同的 loss 曲線;不同 seed 會產生不同曲線。
「另一個 run」包含伺服器正在執行的每張圖,也包含無介面的 POST /api/graph/run/{name} 呼叫。seeded run 執行時,這類呼叫會等待;seeded run 也會等待已執行中的無介面呼叫。未設定 seed 的無介面呼叫仍可彼此並行。
seeded run 的成本如下:在有多個獨立分支的圖上,執行速度約慢 3 到 4 倍;在一般多為線性的教學圖上,差異接近零。即使從畫布啟動,seeded run 也可能等待長時間工作結束。可重現性是選用功能;啟用後會優先確保結果正確,而不是縮短等待時間。
決定性演算法是另一半。它會要求 PyTorch 使用每次都以相同方式組合這些亂數值的運算核心(torch.use_deterministic_algorithms(True, warn_only=True))。刻意設為 warn_only:沒有決定性實作的運算會印出警告,而不是讓 run 失敗,因此你得到的是「所有能重現的部分都已重現」,而不是一段來自 cuDNN 內部的 stack trace。
seed 會記錄在 run 中,並顯示於執行任務面板,因此可以查出特定結果所使用的設定。
從命令列:
cdui run graph.json --seed 1234 --deterministic
種子固定的是「軟體層面」的亂數。位元完全一致的保證只適用於 CPU;換不同的 GPU、驅動程式版本或 PyTorch 版本時,浮點數的歸約順序仍然可能不同。
停止
點擊 停止 可取消執行中的 run。只有「停止」會取消 run。
關閉瀏覽器分頁、前往其他頁面或中斷連線,都不會停止 run。run 由伺服器持有,不依附於 WebSocket 連線。返回後,分頁會重新連線到先前觀看的 run,將錯過的事件重播到結果面板,再繼續即時跟隨。因此,重新整理頁面、關上筆電或短暫的 Wi-Fi 中斷,都不會終止長時間訓練。你也可以在執行任務面板中按觀看,以相同方式讓分頁跟隨伺服器持有的其他 run。
取消採用協作方式,不會立即中斷,因為任意節點程式碼執行到一半時無法安全停止。長時間執行的節點會在每個 batch、step 或 item 檢查取消狀態。訓練迴圈會在一個 batch 內停止,並在結束前寫入 interrupt checkpoint。其他節點會完成目前的呼叫,run 再於下一個節點邊界停止。兩種情況都會記錄為 cancelled。
瀏覽器之外
cdui run會把已儲存的圖送到執行中的伺服器,並從終端機跟隨進度;關閉終端機後 run 仍會繼續。- CLI 圖形執行器 不需要伺服器,會直接在行程內執行圖。
- 匯出為 Python(分頁與持久化)會寫出獨立程式,並委派給相同的節點實作。匯出時會清空 secret 參數;
--seed預設採用匯出時畫布設定的 seed(使用--no-seed可改用新的熵來源);--timeout是 soft timeout,已在執行的節點會完成,但不會再啟動下一個節點。 - TensorBoard —
TrainingLoop.tensorboard會將 event 檔寫入該 run 的產出目錄。