從單機 Python 到 AI 叢集:Ray 如何把擴展成本藏進程式模型
當機器學習工作負載從單機走向分散式,真正棘手的往往不是把 CPU 數量加大,而是重新設計任務、狀態、資料與服務邊界。Ray 以 tasks、actors 與 objects 建立一套 Python 原生的分散式執行模型,再往上提供 Data、Train、Tune、RLlib 與 Serve 等 AI libraries。本文用一個可驗證的上手範例,拆解它適合解決什麼問題,以及哪些情境不該急著導入。
從單機 Python 到 AI 叢集:Ray 如何把擴展成本藏進程式模型
很多機器學習專案的第一版都很順利:在筆電上讀資料、呼叫模型、跑一個訓練迴圈,幾分鐘後就能看到結果。真正的問題通常發生在第二版。資料量變大、實驗次數增加、模型推論需要併發,原本單機上的 Python 程式開始被記憶體、CPU、GPU 或等待時間卡住。此時團隊面對的不是單純「換一台更大的機器」,而是要重新處理任務分派、狀態保存、資料傳遞、失敗重試與服務部署。
Ray 是一個很適合用來理解這個轉折點的開源專案。官方 README 將它定位為統一的 AI 與 Python 應用擴展框架:底層是分散式 runtime,上層則提供 Data、Train、Tune、RLlib 與 Serve 等 AI libraries。它的目標不是把每個人都變成分散式系統專家,而是讓相同的 Python 程式可以先在筆電執行,再逐步搬到叢集。
截至 2026 年 9 月 1 日,GitHub repository 顯示 Ray 已累積超過四萬顆 Star,且仍在近期持續推送。對 AI Chain 讀者而言,它的價值不只在於「能跑更多 GPU」,而在於提供一個清楚的工程抽象:先描述工作,再決定工作要落在哪些資源上。
先講結論:Ray 解決的是執行層,不是所有資料與模型問題
我會先把 Ray 的角色說清楚。它不是一個新的模型,也不是單獨的資料庫或容器平台;它比較像把 Python 工作負載搬進分散式執行環境的底座。你仍然需要選擇模型、資料格式、儲存系統、部署環境與監控方式,但 Ray 可以把任務平行化、長駐狀態、叢集物件與服務部署這些工作,放進一致的程式介面。
這個定位很重要,因為「分散式」不等於「一定更快」。如果任務太小,跨程序與跨機器的協調成本可能比計算本身更高;如果資料搬移太頻繁,網路就會成為瓶頸;如果工作沒有明確的失敗邊界,叢集只會把問題放大。Ray 提供的是可擴展的執行模型,效能與可靠性仍然需要由應用程式設計來負責。
Ray Core 的三個基本抽象
官方 README 用 tasks、actors 與 objects 說明 Ray Core。這三個概念足以建立第一個心智模型。
Tasks:把無狀態函式交給叢集
Task 適合描述一次性的、沒有長期內部狀態的工作,例如處理一個檔案、計算一批特徵、呼叫一次模型或對一個資料分片做轉換。你可以在 Python 函式上加上 @ray.remote,然後用 .remote() 送出工作。函式的回傳值不是直接的 Python 物件,而是 Ray object reference;這讓 runtime 有機會安排依賴與執行位置。
import ray
ray.init()
@ray.remote
def square(value):
return value * value
references = [square.remote(i) for i in range(8)]
results = ray.get(references)
print(results)
ray.shutdown()這段程式的重點不是把 square 寫得多複雜,而是呼叫端不需要自己建立 thread pool、處理程序佇列或追蹤每個工作。等到要連到叢集時,程式的分工方式仍可維持,改變的是 Ray 的初始化與資源配置。
Actors:讓有狀態的 worker 長駐
有些工作不能只靠一個個獨立函式完成,例如模型需要載入一次後反覆推論、資料庫連線要保持、或某個 worker 需要累積快取。Actor 將一個 Python class 變成長駐的遠端程序,方法呼叫會在這個狀態所在的 worker 上執行。
import ray
ray.init()
@ray.remote
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
return self.value
counter = Counter.remote()
print(ray.get(counter.increment.remote()))
print(ray.get(counter.increment.remote()))
ray.shutdown()對 LLM 或影像模型服務來說,Actor 的直覺用途是把昂貴的載入動作從每次請求中移走。不過,狀態也代表新的責任:Actor 失敗後如何恢復,請求是否能重試,狀態是否需要持久化,以及多個 Actor 之間是否會產生一致性問題,都不能只靠 decorator 解決。
Objects:在工作之間傳遞不可變結果
Task 與 Actor 之間常常需要交換大型資料。Ray object store 讓結果可以由多個工作透過 object reference 取得,而不是每次都把完整 Python 物件複製到呼叫端。這是 Ray 能處理資料與模型工作負載的關鍵之一,但它不會消除記憶體限制。大型 object 仍然需要放進可用的 object store,生命週期與資料大小仍應被監控。
AI libraries 把相同底座變成不同工作流
只使用 Ray Core 可以建立自訂分散式程式,但多數 AI 團隊需要的是更高階的工作流。官方列出的 AI libraries,反映了幾種常見的擴展方向。
- Ray Data:處理可擴展的資料集與機器學習資料處理。它適合把資料讀取、轉換、批次推論或前處理拆成可平行的階段。
- Ray Train:支援分散式訓練,把 worker 啟動、資料分片與訓練協調抽離出來。
- Ray Tune:把超參數搜尋與實驗管理變成可平行的試驗集合,避免手動逐一等待。
- RLlib:針對可擴展的 reinforcement learning 工作負載提供元件。
- Ray Serve:把模型或 Python 函式包成可程式化的 serving 層,處理部署、路由與擴展相關問題。
這些 library 不應被看成五個互不相關的產品。它們共用 Ray 的 runtime 與資源概念,因此資料前處理、訓練、調參與服務之間有機會使用一致的部署方式。另一方面,也不要為了「全套整合」而把所有 library 一次導入;先解決目前最大的瓶頸,通常比較容易維護。
從筆電到叢集,真正改變的是邊界
Ray 的吸引力在於同一份 Python 程式可以從本地開始。官方 README 的安裝入口是 pip install ray,文件則提供安裝頁與各個 library 的 getting started。第一次嘗試時,我會先把範例限制在單機模式,確認任務依賴、回傳結果與錯誤處理,再啟動本地叢集或連到遠端環境。
一個可靠的上手順序如下:
- 安裝與版本固定:確認 Python、Ray 與底層 ML framework 的版本,避免本地與叢集使用不同套件。
- 先跑最小 Task:用少量輸入確認
.remote()、ray.get()與例外處理行為。 - 加入資源宣告:只有當工作真的需要 GPU 或多個 CPU 時,才透過 Ray 的資源選項描述需求。
- 觀察 Dashboard:官方文件提供 Ray Dashboard,可用來查看應用程式、叢集與資源使用情況。
- 再移到叢集:把初始化、環境封裝、資料路徑、網路與權限一項項驗證,不要把「本地能跑」當成叢集已準備完成。
若程式直接在本機執行卻無法在叢集工作,常見原因不一定是 Ray API。檢查項目包括 worker 是否能取得相同的 Python 依賴、資料路徑是否只存在於 driver、物件是否太大、GPU 資源名稱是否符合環境,以及 firewall 或 Kubernetes 網路是否阻擋節點互通。
一個 AI 推論管線可以怎麼拆
假設我們要對大量文件做 embedding 或摘要,單機版本可能是讀檔、載入模型、迴圈處理、寫回結果。用 Ray 思考時,可以先把流程拆成三段:資料讀取與切片由 Ray Data 處理;模型載入與批次推論由 actor 或高階推論介面處理;結果寫入則透過明確的 output sink 完成。
這種拆法比「把整個 main function 加上 remote」更健康,因為每個階段的輸入輸出、資源需求與失敗行為都清楚。資料階段應能重跑,模型 worker 應避免重複載入,寫入階段應該具備冪等性。當某個批次失敗時,系統才有機會只重試失敗部分,而不是從頭重做全部工作。
對 LLM serving 也一樣。Ray Serve 可以作為模型服務的部署抽象,但模型推論延遲、batching、併發上限、GPU 記憶體與流量尖峰仍然需要壓測。把請求送進叢集,不會自動讓 token throughput 變好;你仍要量測 queue time、model time、first token latency、完整回應延遲與錯誤率。
我會先檢查的五個工程風險
資料搬移
分散式任務最容易被忽略的是資料位置。若每個 worker 都要從 driver 拿一份大型資料,網路與記憶體會迅速成為瓶頸。優先讓 worker 就地讀取可共享的資料來源,或明確使用 object reference 管理資料生命週期。
失敗與重試
重試不是免費的。對外部 API、寫入資料庫或非冪等操作,重試可能造成重複請求與重複結果。每個 task 或 actor 方法都應先定義可以安全重試的範圍,並把失敗記錄帶上輸入識別碼。
資源隔離
同一個叢集可能同時跑訓練、推論與資料處理。如果沒有清楚宣告 CPU、GPU、記憶體與併發限制,一個工作流就可能搶走其他服務的資源。資源配置應和 SLO、成本與優先級一起設計。
可觀測性
Dashboard 是入口,不是完整監控策略。正式環境還需要集中式 logs、metrics、trace、任務輸入輸出摘要與版本資訊,才能回答某一批結果為何變慢或變錯。
版本與環境
分散式執行會放大依賴不一致問題。Ray 的 runtime 能協助排程,但你仍要管理 runtime environment、容器映像、模型檔案與資料 schema。把環境定義放進可版本控制的設定,比在每台 worker 手動安裝套件可靠得多。
什麼時候不該使用 Ray
如果工作負載是一個很小的同步腳本,執行時間短、資料量小、沒有併發需求,直接使用 Python 或一個簡單的 job runner 通常更好。導入分散式 runtime 會增加依賴、除錯面與部署成本,不能只因為「未來可能變大」就提前支付。
如果團隊沒有能力維護叢集網路、資源配額、版本與監控,也應先評估託管服務或更窄的專用工具。Ray 的彈性很高,但彈性本身也是責任。尤其在涉及個資、金融資料或高風險模型時,權限、資料落地位置與隔離政策應先於效能決策。
結語:把擴展從重寫系統,變成重新描述工作
Ray 最值得學習的地方,是它把單機 Python 與分散式 AI 應用之間的距離,縮小成一組可理解的抽象:無狀態工作用 tasks,長駐狀態用 actors,跨工作傳遞結果用 objects,再用 Data、Train、Tune、RLlib 與 Serve 對應常見的 AI 工作流。
這並不代表所有程式都應該立刻搬進叢集。我的建議是從一個有明確瓶頸、可量測輸入輸出、可以安全重試的工作開始,先在本地驗證,再逐步加入資源宣告、Dashboard、叢集與正式監控。當程式的執行邊界被描述清楚,擴展才不會只是增加機器,而是讓系統在資料量、實驗數量與服務流量增加時,仍然保持可理解、可觀測與可維護。
參考資料