Протокол удалённых ссылок
Создано: 7 мая 2026 г. | Последнее обновление: 7 мая 2026 г.
В этой заметке описаны детали проектирования протокола удалённых ссылок и рассмотрены потоки сообщений в различных сценариях. Прежде чем продолжить, ознакомьтесь с Распределённая RPC-инфраструктура.
Общие сведения
RRef означает Remote REFerence (удалённая ссылка). Это ссылка на объект, расположенный на локальном или удалённом рабочем узле; она прозрачно обрабатывает подсчёт ссылок. Концептуально её можно рассматривать как распределённый указатель с совместным доступом. Приложения могут создать RRef, вызвав remote(). Каждый RRef принадлежит рабочему узлу, на котором выполняется вызываемая сторона вызова remote() (то есть владельцу), и может использоваться несколькими пользователями. Владелец хранит реальные данные и отслеживает глобальное количество ссылок. Каждый RRef можно однозначно идентифицировать с помощью глобального RRefId, который назначается при создании на вызывающей стороне вызова remote().
На рабочем узле владельца существует только один экземпляр OwnerRRef, содержащий реальные данные, тогда как на рабочих узлах пользователей может быть сколько угодно экземпляров UserRRefs, а UserRRef не хранит данные. При любом использовании на стороне владельца извлекается уникальный экземпляр OwnerRRef по глобально уникальному RRefId. Экземпляр UserRRef создаётся, когда он используется в качестве аргумента или возвращаемого значения при вызове rpc_sync(), rpc_async() или remote(), после чего владелец уведомляется для обновления счётчика ссылок. Экземпляр OwnerRRef и его данные удаляются, когда глобально не остаётся экземпляров UserRRef, а на стороне владельца нет ссылок на OwnerRRef.
Предположения
Протокол RRef разработан с учётом следующих предположений.
- Временные сбои сети: при временных сбоях сети в рамках RRef сообщения отправляются повторно. Сбои узлов и постоянные разделения сети не обрабатываются. В таких случаях приложение должно остановить все рабочие узлы, вернуться к предыдущей контрольной точке и продолжить обучение.
-
Неидемпотентные UDF: предполагается, что пользовательские функции (UDF), передаваемые в
rpc_sync(),rpc_async()илиremote(), не являются идемпотентными и поэтому не могут вызываться повторно. Однако внутренние управляющие сообщения RRef идемпотентны и отправляются повторно при сбое передачи сообщения. - Доставка сообщений не по порядку: мы не предполагаем, что сообщения между любой парой узлов доставляются по порядку, поскольку и отправитель, и получатель используют несколько потоков. Нет гарантии, какое сообщение будет обработано первым.
Время жизни RRef
Цель протокола — удалять OwnerRRef в подходящий момент. Удалять OwnerRRef следует тогда, когда не осталось действующих экземпляров UserRRef и пользовательский код больше не хранит ссылок на OwnerRRef. Сложность заключается в том, чтобы определить, остались ли действующие экземпляры UserRRef.
Обоснование проектирования
Пользователь может получить UserRRef в трёх случаях:
- Получение
UserRRefот владельца. - Получение
UserRRefот другого пользователя. - Создание нового
UserRRef, принадлежащего другому рабочему узлу.
Случай 1 — самый простой: владелец передаёт свой RRef пользователю, вызывая rpc_sync(), rpc_async() или remote() и используя свой RRef в качестве аргумента. В этом случае на стороне пользователя создаётся новый UserRRef. Поскольку владелец является вызывающей стороной, он может легко обновить локальный счётчик ссылок на OwnerRRef.
Единственное требование — любой UserRRef должен уведомить владельца при удалении. Поэтому необходимо следующее первое условие:
G1. Владелец будет уведомлён об удалении любого UserRRef.
Поскольку сообщения могут задерживаться или приходить не по порядку, необходимо ещё одно условие, гарантирующее, что сообщение об удалении не будет обработано преждевременно. Если A отправляет B сообщение, связанное с RRef, RRef на стороне A (родительский RRef) и RRef на стороне B (дочерний RRef) называются соответственно родительским и дочерним.
G2. Родительский RRef НЕ будет удалён, пока дочерний RRef не будет подтверждён владельцем.
В случаях 2 и 3 владелец может иметь лишь частичную информацию о графе ветвления RRef или не иметь её вовсе. Например, RRef может быть создан на пользовательском узле, и до того, как владелец получит какой-либо RPC-вызов, создавший его пользователь может уже поделиться RRef с другими пользователями, а те, в свою очередь, могут передать его дальше. Один из инвариантов состоит в том, что граф ветвления любого RRef всегда является деревом: при ветвлении RRef на вызываемой стороне всегда создаётся новый экземпляр UserRRef (за исключением случая, когда вызываемая сторона — владелец), поэтому у каждого RRef только один родитель.
Представление владельца о любом UserRRef в дереве проходит три этапа:
1) unknown -> 2) known -> 3) deleted.
Представление владельца обо всём дереве постоянно меняется. Владелец удаляет свой экземпляр OwnerRRef, когда считает, что действующих экземпляров UserRRef больше нет; то есть после удаления OwnerRRef все экземпляры UserRRef либо действительно удалены, либо неизвестны владельцу. Опасный случай возникает, когда некоторые ветвления неизвестны, а другие уже удалены.
G2 очевидным образом гарантирует, что родительский UserRRef не может быть удалён до того, как владелец узнает обо всех его дочерних экземплярах UserRRef. Однако дочерний UserRRef может быть удалён до того, как владелец узнает о его родительском UserRRef.
Рассмотрим следующий пример: OwnerRRef ветвится на A, затем A ветвится на Y, а Y — на Z:
OwnerRRef -> A -> Y -> Z
Если все сообщения от Z, включая сообщение об удалении, будут обработаны владельцем раньше сообщений от Y, владелец узнает об удалении Z, прежде чем узнает о существовании Y. Тем не менее это не вызовет проблем. По крайней мере один из предков Y (A) останется активным и не позволит владельцу удалить OwnerRRef. Точнее, если владельцу неизвестен Y, A не может быть удалён согласно G2, а владелец знает об A, поскольку является его родителем.
Ситуация становится немного сложнее, если RRef создаётся на пользовательском узле:
OwnerRRef
^
|
A -> Y -> Z
Если Z вызывает to_here() для UserRRef, владелец как минимум знает об A к моменту удаления Z, поскольку иначе to_here() не завершился бы. Если Z не вызывает to_here(), владелец может получить все сообщения от Z раньше любого сообщения от A и Y. В этом случае реальные данные OwnerRRef ещё не созданы, поэтому удалять нечего. Это равносильно отсутствию Z. Следовательно, проблем не возникает.
Реализация
G1 реализуется отправкой сообщения об удалении в деструкторе UserRRef. Для обеспечения G2 родительский UserRRef помещается в контекст при ветвлении; ключом служит новый ForkId. Родительский UserRRef удаляется из контекста только после получения сообщения-подтверждения (ACK) от дочернего, а дочерний отправляет ACK только после подтверждения владельцем.
Сценарии протокола
Теперь рассмотрим, как описанные выше решения реализуются в протоколе в четырёх сценариях.
© 2026, PyTorch Contributors
PyTorch has a BSD-style license, as found in the LICENSE file.
https://docs.pytorch.org/docs/2.14/rpc/rref.html