原文: https://pytorch.org/docs/stable/notes/rref.html
警告
RRef API 是實(shí)驗(yàn)性的,隨時(shí)可能更改。
本說(shuō)明描述了遠(yuǎn)程引用協(xié)議的設(shè)計(jì)細(xì)節(jié),并逐步介紹了不同情況下的消息流。 在繼續(xù)之前,請(qǐng)確保您熟悉分布式 RPC 框架。
RRef 代表遠(yuǎn)程參考。 它是位于本地或遠(yuǎn)程工作人員上的對(duì)象的引用,并且透明地在內(nèi)部進(jìn)行引用計(jì)數(shù)。 從概念上講,它可以視為分布式共享指針。 應(yīng)用程序可以通過(guò)調(diào)用 remote()
創(chuàng)建 RRef。 每個(gè) RRef 都由 remote()
呼叫的被調(diào)用方工作者(即所有者)擁有,并且可以由多個(gè)用戶使用。 所有者存儲(chǔ)實(shí)際數(shù)據(jù),并跟蹤全局參考計(jì)數(shù)。 每個(gè) RRef 可以由全局RRefId
唯一標(biāo)識(shí),該全局RRefId
在創(chuàng)建時(shí)在 remote()
調(diào)用的調(diào)用方上分配。
在所有者工作程序中,只有一個(gè)OwnerRRef
實(shí)例包含真實(shí)數(shù)據(jù),而在用戶工作程序中,可以根據(jù)需要包含任意數(shù)量的UserRRefs
,而UserRRef
不保存數(shù)據(jù)。 所有者上的所有用法都將使用全局唯一的RRefId來(lái)檢索唯一的OwnerRRef實(shí)例。 在 rpc_sync()
, rpc_async()
或 remote()
調(diào)用中將UserRRef
用作參數(shù)或返回值時(shí),將創(chuàng)建該UserRRef
將會(huì)根據(jù)更新的參考計(jì)數(shù)通知所有者。
如果全局沒有UserRRef
實(shí)例,并且所有者上也沒有對(duì)OwnerRRef
的引用,則OwnerRRef
及其數(shù)據(jù)將被刪除。
RRef 協(xié)議的設(shè)計(jì)基于以下假設(shè)。
rpc_sync()
, rpc_async()
或 remote()
的用戶功能(UDF) 不是冪等的,因此無(wú)法重試。 但是,內(nèi)部 RRef 控制消息將成為冪等且可重試。該協(xié)議的目標(biāo)是在適當(dāng)?shù)臅r(shí)候刪除OwnerRRef
。 刪除OwnerRRef
的正確時(shí)機(jī)是在沒有活動(dòng)UserRRef
實(shí)例且用戶代碼也沒有保存對(duì)OwnerRRef
的引用的情況下。 棘手的部分是確定是否存在任何活動(dòng)的UserRRef
實(shí)例。
用戶可以在以下三種情況下獲得UserRRef
:
UserRRef
。UserRRef
。UserRRef
。情況 1 是最簡(jiǎn)單的,所有者將其 RRef 傳遞給用戶,所有者調(diào)用 rpc_sync()
, rpc_async()
或 remote()
使用其 RRef 作為參數(shù)。 在這種情況下,將在用戶上創(chuàng)建一個(gè)新的UserRRef
。 由于所有者是調(diào)用者,因此可以輕松地在OwnerRRef
上更新其本地引用計(jì)數(shù)。
唯一的要求是任何UserRRef
必須在銷毀時(shí)通知所有者。 因此,我們需要第一個(gè)保證:
G1。 刪除任何“ UserRRef”時(shí),都會(huì)通知所有者。
由于郵件可能會(huì)延遲或出現(xiàn)亂序,因此我們還需要一項(xiàng)保證,以確保刪除郵件不會(huì)過(guò)早處理。 如果 A 向 B 發(fā)送涉及 RRef 的消息,我們將 A 上的 RRef 稱為父 RRef,將 B 上的 RRef 稱為子 RRef。
G2。 在所有者確認(rèn)子 RRef 之前,不會(huì)刪除父 RRef。
在情況 2 和 3 中,所有者可能僅對(duì) RRef 分支圖有部分了解或根本不了解。 例如,可以在用戶上構(gòu)建 RRef,并且在所有者收到任何 RPC 調(diào)用之前,創(chuàng)建者用戶可能已經(jīng)與其他用戶共享了 RRef,并且這些用戶可以進(jìn)一步共享 RRef。 一個(gè)不變性是,任何 RRef 的派生圖始終都是一棵樹,因?yàn)榕缮?RRef 總是在被調(diào)用方上創(chuàng)建一個(gè)新的UserRRef
實(shí)例(除非被調(diào)用方是所有者),因此每個(gè) RRef 都有一個(gè)父級(jí)。
所有者對(duì)樹中任何UserRRef
的視圖分為三個(gè)階段:
1) unknown -> 2) known -> 3) deleted.
所有者對(duì)整棵樹的看法不斷變化。 擁有者認(rèn)為沒有活動(dòng)的UserRRef
實(shí)例時(shí),即刪除OwnerRRef
實(shí)例時(shí),所有UserRRef
實(shí)例都可能確實(shí)被刪除或未知,因此所有者刪除了其OwnerRRef
實(shí)例。 危險(xiǎn)的情況是某些分叉未知,而另一些被刪除。
G2 簡(jiǎn)單地保證在擁有者知道其所有子級(jí)UserRRef
實(shí)例之前,不能刪除任何父級(jí)UserRRef
。 但是,有可能在擁有者知道其父項(xiàng)UserRRef
之前刪除了子項(xiàng)UserRRef。
考慮下面的示例,其中OwnerRRef
分支到 A,然后 A 分支到 Y,Y 分支到 Z:
OwnerRRef -> A -> Y -> Z
如果所有者在處理所有來(lái)自 Z 的消息(包括刪除消息)之前先處理來(lái)自 Y 的所有消息,那么所有者將在知道 Y 之前就知道 Z 的刪除。但這不會(huì)造成任何問題。 因?yàn)?,Y 的祖先中至少有一個(gè)還活著(在本例中為 A),這將阻止所有者刪除OwnerRRef
。 更具體地說(shuō),如果所有者不知道 Y,則由于 G2 而無(wú)法刪除 A,并且所有者知道 A,因?yàn)樗姓呤?A 的父母。
如果在用戶上創(chuàng)建 RRef,事情會(huì)變得有些棘手:
OwnerRRef
^
|
A -> Y -> Z
如果 Z 在UserRRef
上調(diào)用 to_here()
,則所有者至少知道刪除 Z 時(shí)的 A,否則, to_here()
不會(huì)結(jié)束。 如果 Z 沒有調(diào)用 to_here()
,則所有者可能在從 A 和 Y 發(fā)送任何消息之前就已從 Z 接收了所有消息。在這種情況下,由于尚未獲得OwnerRRef
的真實(shí)數(shù)據(jù) 創(chuàng)建后,也沒有要?jiǎng)h除的內(nèi)容。
就像 Z 根本不存在一樣。 因此,仍然可以。
G1 通過(guò)在UserRRef
析構(gòu)函數(shù)中發(fā)送刪除消息來(lái)實(shí)現(xiàn)。 為了提供 G2 ,無(wú)論何時(shí)將父級(jí)UserRRef
派生,都將其置于上下文中,并由新的ForkId
對(duì)其進(jìn)行索引。 僅當(dāng)父級(jí)UserRRef
從子級(jí)收到確認(rèn)消息(ACK)時(shí),才會(huì)從上下文中刪除該父級(jí)UserRRef
,并且只有當(dāng)所有者確認(rèn)后,該子級(jí)才會(huì)發(fā)出 ACK。
現(xiàn)在,讓我們討論以上設(shè)計(jì)如何在四種情況下轉(zhuǎn)換為協(xié)議。
import torch
import torch.distributed.rpc as rpc
## on worker A
rref = rpc.remote('B', torch.add, args=(torch.ones(2), 1))
## say the rref has RRefId 100 and ForkId 1
rref.to_here()
在這種情況下,在用戶工作程序 A 上創(chuàng)建UserRRef
,然后將其與遠(yuǎn)程消息一起傳遞給所有者工作程序 B,然后 B 創(chuàng)建OwnerRRef
。 方法 remote()
立即返回,這意味著UserRRef
可以在所有者了解之前被分叉/使用。
在所有者上,當(dāng)接收到 remote()
調(diào)用時(shí),它將創(chuàng)建OwnerRRef
,并返回一個(gè) ACK 來(lái)確認(rèn){100, 1}
(RRefId
,ForkId
)。 僅在收到此 ACK 后,A 才能刪除其UserRRef
。 這涉及 G1 和 G2 。 G1 很明顯。 對(duì)于 G2 而言,OwnerRRef
是UserRRef
的子級(jí),并且UserRRef
直到收到所有者的
ACK 才被刪除。
上圖顯示了消息流,其中實(shí)心箭頭包含用戶功能,而虛線箭頭是內(nèi)置消息。 請(qǐng)注意,從 A 到 B 的前兩個(gè)消息 (remote()
和 to_here()
)可以按任何順序到達(dá) B,但最終的刪除消息僅在以下情況下發(fā)送: :
UserRRef {100, 1}
(G2),并且UserRRef
實(shí)例。 當(dāng) RRef 不再在范圍內(nèi)并且可以進(jìn)行垃圾回收時(shí),就會(huì)發(fā)生這種情況。import torch
import torch.distributed.rpc as rpc
## on worker A and worker B
def func(rref):
pass
## on worker A
rref = rpc.remote('B', torch.add, args=(torch.ones(2), 1))
## say the rref has RRefId 100 and ForkId 1
rpc.rpc_async('B', func, args=(rref, ))
在這種情況下,在 A 上創(chuàng)建UserRRef
后,A 會(huì)將其用作對(duì) B 的后續(xù) RPC 調(diào)用中的參數(shù)。A 將使UserRRef {100, 1}
保持活動(dòng)狀態(tài),直到收到 B 的確認(rèn) (G2 , 而不是 RPC 調(diào)用的返回值)。 這是必要的,因?yàn)樵诮邮盏剿邢惹暗南⒅?,A 不應(yīng)發(fā)出刪除消息,否則,OwnerRRef
可以在使用前刪除,因?yàn)槲覀儾荒鼙WC消息的傳遞順序。 這是通過(guò)創(chuàng)建 RRef 的子項(xiàng)ForkId
并將其保存在地圖中,直到收到所有者確認(rèn)該子項(xiàng)ForkId來(lái)完成的。
下圖顯示了消息流。
注意,UserRRef
可以在功能完成甚至啟動(dòng)之前在 B 上刪除。 但是,這是可以的,因?yàn)樵?B 向子ForkId
發(fā)送 ACK 時(shí),它已經(jīng)獲取了OwnerRRef
實(shí)例,這將防止它被過(guò)早刪除。
所有者對(duì)用戶是最簡(jiǎn)單的情況,所有者可以在本地更新引用計(jì)數(shù),并且不需要任何其他控制消息即可通知其他人。 關(guān)于 G2 ,因?yàn)楦讣?jí)是所有者,所以它與父級(jí)立即從所有者接收 ACK 相同。
import torch
import torch.distributed.rpc as RRef, rpc
## on worker B and worker C
def func(rref):
pass
## on worker B, creating a local RRef
rref = RRef("data")
## say the rref has RRefId 100
dist.rpc_async('C', func, args=(rref, ))
上圖顯示了消息流。 請(qǐng)注意,當(dāng)在 rpc_async 調(diào)用之后OwnerRRef
退出作用域時(shí),將不會(huì)刪除它,因?yàn)槿绻嬖谌魏我阎呐缮瑒t內(nèi)部會(huì)存在一個(gè)使它保持活動(dòng)狀態(tài)的映射,在這種情況下為UserRRef {100, 1}
。 (G2 )
這是最復(fù)雜的情??況,其中調(diào)用者用戶(父級(jí)UserRRef
),被調(diào)用者用戶(子級(jí)UserRRef
)和所有者都需要參與。
import torch
import torch.distributed.rpc as rpc
## on worker A and worker C
def func(rref):
pass
## on worker A
rref = rpc.remote('B', torch.add, args=(torch.ones(2), 1))
## say the rref has RRefId 100 and ForkId 1
rpc.rpc_async('C', func, args=(rref, ))
當(dāng) C 從 A 接收到子項(xiàng)UserRRef
時(shí),它向所有者 B 發(fā)送一個(gè)派生請(qǐng)求。稍后,當(dāng) B 確認(rèn) C 上的UserRRef
時(shí),C 將并行執(zhí)行兩個(gè)操作:1)發(fā)送子項(xiàng) ACK 到 A,然后 2)運(yùn)行用戶提供的功能。 在這段時(shí)間中,親本(A)將保持其UserRRef {100, 1}
存活以實(shí)現(xiàn) G2 。
更多建議: