Skip to content

vllm.distributed.weight_transfer.sharded_rdt_trainer

Trainer-side engine for the sharded-RDT (pull-based NIXL) backend.

RDT is pull-based, so unlike NCCL this engine broadcasts nothing. It owns a per-rank producer server (an internal Ray actor exposing the NIXL serve surface the worker engine dials by name), and on each send_weights gathers this rank's weights group-by-group from the WeightSource, shares each group into the server over CUDA IPC, and — on the sender — drives the inference-side start/update/finish handshake, whose single empty update_weights unblocks the workers to pull.

All serve-side state lives on the server actor, so trainer processes need no mixin, no named actors and no special actor options.

See docs/training/weight_transfer/sharded_rdt.md for the publish -> serve -> free_group -> release lifecycle and the ownership model.

Classes:

ShardedRDTTrainerInitInfo dataclass

Bases: TrainerInitInfo

Trainer init info for the sharded-RDT backend.

Identical on every rank except rank (rank 0 is the sender). Carries only the must-agree wire params; the sender forwards them verbatim onto the worker-side init info so the two cannot drift. Server-actor names are generated per rank and all-gathered by the engine, not supplied here.

Attributes:

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
@dataclass
class ShardedRDTTrainerInitInfo(TrainerInitInfo):
    """Trainer init info for the sharded-RDT backend.

    Identical on every rank except ``rank`` (rank 0 is the sender). Carries only
    the must-agree wire params; the sender forwards them verbatim onto the
    worker-side init info so the two cannot drift. Server-actor names are
    generated per rank and all-gathered by the engine, not supplied here.
    """

    backend: ClassVar[str] = "sharded_rdt"

    num_consumers: int
    """Total inference-worker (consumer) count across the whole fleet
    (DP*TP*PP*PCP), for the M:N block assignment / free ref-count."""
    workers_per_replica: int = 0
    """Consumers per inference DEPLOYMENT (``num_consumers // num_replicas``).

    Fixes the slot-sharing groups: consumers whose ids differ by a multiple of
    this are the same worker of different deployments, so they bake identical
    plans and pull byte-identical chunks, and ONE registered serve slot can
    serve all of them. 0 disables sharing, and so does the single-deployment
    value ``num_consumers``, which makes every group a singleton and the serve
    path identical to the unshared one.
    """
    trainer_actor_namespace: str | None = None
    """Ray namespace the engine spawns its serve actors in. The inference
    workers (which run in their own EngineCore subprocess with its own
    ``ray.init``) resolve those actors by name, so this must be the namespace
    they can see. Forwarded to the worker-side init info."""
    num_rdt_buffers: int = 2
    """Serve/receive ring depth K (must match the worker)."""
    buffer_presize_gb: float = 0.0
    """Serve-buffer pre-size floor in GiB (avoids NIXL desc-cache churn)."""
    gather_lookahead: int = DEFAULT_GATHER_LOOKAHEAD
    """Gathered-but-unfreed groups the gather loop may run ahead by. Bounds
    trainer-resident memory at ``gather_lookahead + 1`` groups."""
    stall_timeout_s: float = DEFAULT_STALL_TIMEOUT_S
    """Seconds of no publish/serve/free progress before the producer fails the
    sync (see ``DEFAULT_STALL_TIMEOUT_S``)."""

buffer_presize_gb = 0.0 class-attribute instance-attribute

Serve-buffer pre-size floor in GiB (avoids NIXL desc-cache churn).

gather_lookahead = DEFAULT_GATHER_LOOKAHEAD class-attribute instance-attribute

Gathered-but-unfreed groups the gather loop may run ahead by. Bounds trainer-resident memory at gather_lookahead + 1 groups.

num_consumers instance-attribute

Total inference-worker (consumer) count across the whole fleet (DPTPPP*PCP), for the M:N block assignment / free ref-count.

num_rdt_buffers = 2 class-attribute instance-attribute

Serve/receive ring depth K (must match the worker).

stall_timeout_s = DEFAULT_STALL_TIMEOUT_S class-attribute instance-attribute

Seconds of no publish/serve/free progress before the producer fails the sync (see DEFAULT_STALL_TIMEOUT_S).

trainer_actor_namespace = None class-attribute instance-attribute

Ray namespace the engine spawns its serve actors in. The inference workers (which run in their own EngineCore subprocess with its own ray.init) resolve those actors by name, so this must be the namespace they can see. Forwarded to the worker-side init info.

workers_per_replica = 0 class-attribute instance-attribute

Consumers per inference DEPLOYMENT (num_consumers // num_replicas).

Fixes the slot-sharing groups: consumers whose ids differ by a multiple of this are the same worker of different deployments, so they bake identical plans and pull byte-identical chunks, and ONE registered serve slot can serve all of them. 0 disables sharing, and so does the single-deployment value num_consumers, which makes every group a singleton and the serve path identical to the unshared one.

ShardedRDTTrainerWeightTransferEngine

Bases: TrainerWeightTransferEngine[ShardedRDTTrainerInitInfo]

Trainer-side engine for the pull-based sharded-RDT backend.

Lives on every trainer rank. Owns a per-rank _RDTProducerServer actor (the NIXL serve surface). send_weights gathers this rank's weights group-by-group from the WeightSource, shares each group into the server over CUDA IPC, and — on the sender — drives the inference-side handshake so the workers pull. Non-sender ranks only gather (staying in the collective).

Methods:

  • get_worker_init_payload

    The consumer-side init payload, rebuilt on demand. Pure — no collective.

  • send_weights

    Gather this rank's weights and publish them for the consumers to pull.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
class ShardedRDTTrainerWeightTransferEngine(
    TrainerWeightTransferEngine[ShardedRDTTrainerInitInfo]
):
    """Trainer-side engine for the pull-based sharded-RDT backend.

    Lives on every trainer rank. Owns a per-rank `_RDTProducerServer` actor
    (the NIXL serve surface). `send_weights` gathers this rank's weights
    group-by-group from the `WeightSource`, shares each group into the server
    over CUDA IPC, and — on the sender — drives the inference-side handshake so
    the workers pull. Non-sender ranks only gather (staying in the collective).
    """

    init_info_cls = ShardedRDTTrainerInitInfo

    def __init__(
        self,
        *,
        client: VLLMWeightSyncClient,
        source: WeightSource,
        is_sender: bool = True,
        init_info: ShardedRDTTrainerInitInfo,
    ) -> None:
        super().__init__(client=client, source=source, is_sender=is_sender)
        self._init_info = init_info
        self._server: Any = None  # the _RDTProducerServer actor handle
        self._server_name: str | None = None
        # Group-major metadata / partition, computed at trainer_init.
        self._meta: list[ParamMeta] = []
        self._groups: list[list[str]] = []
        # Ownership resolved at trainer_init from the fleet's held names: the
        # distinct owner sets, the per-name index into them, this rank's groups
        # and its held set. Routing itself is consumer-side; the trainer only
        # ships the table.
        self._owner_sets: list[list[int]] = []
        self._name_owner_class: list[int] = []
        self._owned_idx: list[int] = []
        self._held_names: set[str] | None = None
        # Strong refs to gathered tensors we've shared into the server, keyed by
        # group index. CUDA-IPC exports must outlive the importer, so we hold
        # them until the server reports the group freed. See send_weights.
        self._inflight: dict[int, dict[str, torch.Tensor]] = {}
        # [RDT-EXPORT-RING] Reusable packed slots, one per resident group. See
        # _pack_group_for_export. Persistent across syncs: freeing and
        # reallocating them each sync fragmented the caching allocator badly
        # enough to grow reserved memory ~1.5 GB per sync.
        self._export_ring: list[torch.Tensor | None] = []
        self._export_ring_args: list[tuple | None] = []
        # Which live group holds which slot, and the slots free to hand out. A
        # modular counter is unsafe here: the credit gate bounds the NUMBER of
        # unfreed groups, not their order, so it can hand a later group the slot
        # a still-live group is being served from.
        self._slot_of_group: dict[int, int] = {}
        self._free_slots: list[int] = []
        # Set by trainer_init from the server-name all-gather; retained so a
        # restarted consumer can be re-initialized without another collective.
        self._server_names: list[str] | None = None

    def _rpc(self, method: str, *args: Any) -> Any:
        """Call one of the server actor's methods and block for the result.
        The single seam through which the engine talks to its server, so tests
        can inject a local (non-Ray) fake server."""
        import ray

        return ray.get(getattr(self._server, method).remote(*args))

    @staticmethod
    def _contig_stride(shape) -> tuple:
        stride: list[int] = []
        acc = 1
        for s in reversed(list(shape)):
            stride.append(acc)
            acc *= int(s)
        return tuple(reversed(stride))

    def _pack_group_for_export(self, held: list, slot_idx: int) -> tuple:
        """Copy ``held`` into ring slot ``slot_idx``; return ``(storages, views,
        refs)`` in the same shape the per-storage path returns, but with ONE
        storage. Views are built exactly as ``publish_group`` rebuilds them, so
        the two sides cannot disagree about layout.

        Slot safety is not the credit gate alone: that bounds how MANY groups
        are unfreed, not the order they free in, and barriers do complete out of
        order. The caller takes ``slot_idx`` from a pool keyed by live group
        (``_slot_of_group`` / ``_free_slots``); ``_drop_inflight`` returns a slot
        only once its group is freed everywhere.
        """
        offsets: list[int] = []
        cur = 0
        for _name, t in held:
            cur = (cur + 15) & ~15  # 16B keeps every element size we ship aligned
            offsets.append(cur)
            cur += t.numel() * t.element_size()
        need = max(16, (cur + 15) & ~15)

        slot = self._export_ring[slot_idx]
        device = held[0][1].device
        if slot is None or slot.numel() < need or slot.device != device:
            self._export_ring[slot_idx] = self._export_ring_args[slot_idx] = None
            slot = torch.empty(need, dtype=torch.uint8, device=device)
            self._export_ring[slot_idx] = slot

        ust = slot.untyped_storage()
        sid = ust.data_ptr()
        reduce_args = self._export_ring_args[slot_idx]
        if reduce_args is None:
            base = torch.empty(0, dtype=torch.uint8, device=device)
            base.set_(ust, 0, (ust.nbytes(),))
            _rebuild, reduce_args = reduce_tensor(base)
            self._export_ring_args[slot_idx] = reduce_args

        views: dict[str, tuple] = {}
        refs: dict[str, torch.Tensor] = {}
        for (name, t), off in zip(held, offsets):
            shape, esz = tuple(t.shape), t.element_size()
            stride = self._contig_stride(shape)
            dst = torch.as_strided(slot.view(t.dtype), shape, stride, off // esz)
            dst.copy_(t)
            refs[name] = dst
            views[name] = (
                sid,
                str(t.dtype).split(".")[-1],
                list(shape),
                list(stride),
                off // esz,
            )
        # The producer sidecar reads this slot from another process over CUDA
        # IPC, while these copies are merely enqueued on this rank's stream;
        # nothing orders the two. Without this sync it packs bytes the copy has
        # not written yet -- silent corruption, and only for ring-packed tensors.
        if slot.is_cuda:
            torch.cuda.current_stream(slot.device).synchronize()
        return {sid: reduce_args}, views, refs

    def _publish_async(self, group_idx: int, entries):
        """Fire publish_group WITHOUT waiting on the RPC (the gather loop
        overlaps the publish's server-side rebuild with the next group's
        gather) and return a handle that ``_await_publish`` resolves. Ray actor
        handle in production; a plain (non-Ray) fake server runs inline."""
        method = self._server.publish_group
        remote = getattr(method, "remote", None)
        if remote is not None:
            return remote(group_idx, entries)
        return method(group_idx, entries)

    def _await_publish(self, ref) -> None:
        """Resolve one async publish so a server-side rebuild error surfaces at
        window depth instead of at end_sync. Publishes carry nothing back —
        freed groups flow only through wait_freed/end_sync (one channel; see
        publish_group)."""
        import ray

        if isinstance(ref, ray.ObjectRef):
            ray.get(ref)

    # ---------------- construction ----------------

    @classmethod
    def trainer_init(
        cls,
        init_info: ShardedRDTTrainerInitInfo,
        *,
        client: VLLMWeightSyncClient,
        source: WeightSource | None = None,
    ) -> Self:
        if source is None:
            raise ValueError(
                "Sharded RDT trainer weight transfer requires a WeightSource."
            )
        engine = cls(
            client=client,
            source=source,
            is_sender=init_info.is_sender,
            init_info=init_info,
        )

        # The VMM-based expandable_segments allocator makes IPC storage opens ~9x
        # slower on both export and rebuild, and CUDA-IPC publish is this engine's
        # hot path. Frameworks enabling it should disable it around send_weights.
        # Only the env var is visible; runtime changes are not introspectable.
        import os as _os

        if "expandable_segments:True" in _os.environ.get("PYTORCH_CUDA_ALLOC_CONF", ""):
            logger.warning(
                "Sharded-RDT trainer: PYTORCH_CUDA_ALLOC_CONF enables "
                "expandable_segments; CUDA-IPC weight publishing will be "
                "several times slower. Disable expandable segments around "
                "weight sync (allocate gather buffers in classic segments)."
            )

        engine._meta = list(source.metadata())
        names = [m.name for m in engine._meta]
        engine._groups = layerwise_groups(names)
        flat = [n for g in engine._groups for n in g]
        if flat != names:
            raise ValueError(
                "Sharded RDT requires a WeightSource whose metadata order is "
                "group-contiguous. Reorder the source so all names sharing a "
                "layer index are adjacent."
            )

        world, rank = engine._world_and_rank()
        engine._resolve_ownership(world, rank)
        engine._spawn_server(sorted(engine._held_names or []))

        # Every rank's server must exist before the sender's init RPC (the worker
        # init calls reserve_serve_buffer back on ALL producer servers). The
        # all-gather of server names doubles as that barrier.
        server_names = engine._all_gather_server_names(world, rank)
        # Retained so a RESTARTED consumer can be re-initialized without another
        # all-gather (see get_worker_init_payload). The uuid actor names stay valid
        # for the run, because producers never restart.
        engine._server_names = server_names

        if engine.is_sender:
            worker_init = engine._build_worker_init_info(server_names)
            engine.client.init_weight_transfer_engine(asdict(worker_init))
        return engine

    def get_worker_init_payload(self) -> dict:
        """The consumer-side init payload, rebuilt on demand. Pure — no collective.

        Reads only retained state, so a restarted inference engine can rejoin at a
        sync boundary: the driver can ask any time, including mid-run with every
        rank in its own training step. A collective here would deadlock, since the
        ranks are not at a matching one.

        Raises:
            RuntimeError: called before ``trainer_init`` cached the server names.
        """
        if self._server_names is None:
            raise RuntimeError(
                "get_worker_init_payload requires trainer_init to have run (the "
                "producer server names are gathered there)."
            )
        return asdict(self._build_worker_init_info(self._server_names))

    def _world_and_rank(self) -> tuple[int, int]:
        if torch.distributed.is_available() and torch.distributed.is_initialized():
            return torch.distributed.get_world_size(), torch.distributed.get_rank()
        return 1, self._init_info.rank

    def _resolve_ownership(self, world: int, rank: int) -> None:
        """Resolve which rank holds which name, and this rank's publish plan.

        A source may hold only part of the model — pipeline stages, expert
        parallelism, or any mix — so each rank declares its held names and the
        fleet all-gathers them. The consumers route per name, so the wire carries
        the transposed result: the distinct owner sets, and a per-name index into
        them.

        The masks are positional over metadata order, which is why the metadata
        digest is checked first: a rank whose names disagree would transpose into
        the wrong owners entirely.
        """
        assert self.source is not None  # guaranteed by trainer_init
        num_groups = len(self._groups)
        names = [m.name for m in self._meta]
        held = self.source.held_names()
        if held is None:
            held_set = set(names)
        else:
            held_set = {str(n) for n in held}
            unknown = sorted(held_set - set(names))
            if unknown:
                raise ValueError(
                    f"WeightSource.held_names() lists {len(unknown)} name(s) not "
                    f"in metadata(), e.g. {unknown[:3]}."
                )
            if not held_set:
                raise ValueError(
                    "WeightSource.held_names() is empty; a rank with nothing to "
                    "serve cannot take part in the gather"
                )

        # One collective carries this rank's holdings as a bitmask over metadata
        # order, plus the digest of the metadata it indexes into. The digest is
        # what makes partial ownership safe: only the sender's metadata reaches
        # the consumers, so a rank describing just its own share would silently
        # serve the wrong weights.
        digest = self._meta_digest()
        mask = bytearray((len(names) + 7) // 8)
        for i, n in enumerate(names):
            if n in held_set:
                mask[i >> 3] |= 1 << (i & 7)
        per_rank = self._all_gather_owned(world, (digest, bytes(mask)))
        mismatched = [r for r, (d, *_rest) in enumerate(per_rank) if d != digest]
        if mismatched:
            raise ValueError(
                f"WeightSource.metadata() disagrees across trainer ranks "
                f"(rank {rank} digest {digest}, differing ranks {mismatched[:4]}). "
                "Every rank must describe the WHOLE model, even when it holds "
                "only some of it."
            )
        masks = [m for _d, m in per_rank]

        # Transpose to per-name owners, then dedup into classes. Numbering by
        # FIRST APPEARANCE in metadata order keeps it a pure function of
        # rank-identical inputs, which is what lets a rejoining consumer rebuild
        # the identical table from get_worker_init_payload.
        owner_sets: list[list[int]] = []
        class_of_owners: dict[tuple[int, ...], int] = {}
        name_owner_class: list[int] = []
        for i, n in enumerate(names):
            owners = tuple(r for r, m in enumerate(masks) if m[i >> 3] & (1 << (i & 7)))
            if not owners:
                raise ValueError(
                    f"no trainer rank holds {n!r}; every name in metadata() must "
                    "be held by at least one rank or it can never be served."
                )
            ci = class_of_owners.get(owners)
            if ci is None:
                ci = len(owner_sets)
                class_of_owners[owners] = ci
                owner_sets.append(list(owners))
            name_owner_class.append(ci)

        self._owner_sets = owner_sets
        self._name_owner_class = name_owner_class
        # Groups holding anything here: exactly what iteration must cover.
        self._owned_idx = [
            gi
            for gi in range(num_groups)
            if any(n in held_set for n in self._groups[gi])
        ]
        self._held_names = held_set

    def _validate_held_yields(self, gi: int, names, tensors) -> None:
        """Check ``held_names()`` against what the source actually yields — the
        one place both sit side by side.

        Without it, a source that claims a name but yields ``None`` for it dies
        by stall watchdog 300s later: consumers route pulls here, the pull
        passes the served-names guard, and the cache wait never completes. With
        it, that is an immediate error naming the weight. One set lookup per
        name per sync."""
        if self._held_names is None:
            return
        for name, tensor in zip(names, tensors):
            if (tensor is None) == (name in self._held_names):
                claim = "does not hold" if tensor is not None else "holds"
                have = "a real tensor" if tensor is not None else "None"
                raise RuntimeError(
                    f"held_names() disagrees with the yielded tensors: group {gi} "
                    f"name {name!r} yielded {have} but this rank {claim} it. A "
                    "WeightSource must yield a real tensor for every held name "
                    "and None for the rest (see WeightSource.held_names)."
                )

    def _meta_digest(self) -> str:
        """Stable digest of this rank's metadata (name order + count)."""
        import hashlib

        h = hashlib.sha256()
        h.update(f"{len(self._meta)}\n".encode())
        for m in self._meta:
            h.update(m.name.encode())
            h.update(b"\n")
        return h.hexdigest()[:16]

    def _all_gather_owned(self, world: int, mine: tuple) -> list[tuple]:
        """All-gather each rank's (metadata digest, held-name bitmask)."""
        if world <= 1 or not (
            torch.distributed.is_available() and torch.distributed.is_initialized()
        ):
            return [mine]
        gathered: list[Any] = [None] * world
        torch.distributed.all_gather_object(gathered, mine)
        return gathered

    def _all_gather_server_names(self, world: int, rank: int) -> list[str]:
        assert self._server_name is not None
        if world <= 1 or not (
            torch.distributed.is_available() and torch.distributed.is_initialized()
        ):
            return [self._server_name]
        gathered: list[str | None] = [None] * world
        torch.distributed.all_gather_object(gathered, self._server_name)
        return [n for n in gathered if n is not None]

    def _spawn_server(self, served_names: list[str]) -> None:
        import ray
        from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy

        # The trainer is usually a separate install from the workers, so it gets
        # its own check rather than trusting the consumer side's.
        check_ray_rdt_version()

        ii = self._init_info
        self._server_name = f"vllm_rdt_producer_{uuid.uuid4().hex[:12]}_rk{ii.rank}"
        node_id = ray.get_runtime_context().get_node_id()
        # Pin the server to this rank's physical GPU: num_gpus=0 so Ray does not
        # allocate a second, CUDA_VISIBLE_DEVICES so CUDA IPC to the rank's
        # gathered tensors works. max_concurrency > 1 serves pulls while control
        # calls are in flight; enable_tensor_transport gives the NIXL serve.
        gpu_ids = ray.get_gpu_ids()
        # The server is the trainer rank's process twin: forward the env the
        # rank runs under (library paths etc.) so it imports torch/vllm the
        # same way, then pin it to the rank's physical GPU for CUDA IPC.
        import os

        env_vars = {
            k: os.environ[k]
            for k in (
                "LD_LIBRARY_PATH",
                "LD_PRELOAD",
                "NCCL_CUMEM_ENABLE",
                "VLLM_NCCL_SO_PATH",
                "PATH",
            )
            if k in os.environ
        }
        if gpu_ids:
            # A num_gpus=0 actor sharing the rank's GPU: Ray would set
            # CUDA_VISIBLE_DEVICES="" and hide every GPU, so tell it not to touch
            # the var and pin the device ourselves.
            env_vars["RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES"] = "1"
            env_vars["CUDA_VISIBLE_DEVICES"] = str(gpu_ids[0])
        runtime_env = {"env_vars": env_vars} if env_vars else {}
        server_cls = ray.remote(_RDTProducerServer).options(
            name=self._server_name,
            namespace=ii.trainer_actor_namespace,
            num_cpus=0,
            num_gpus=0,
            # Thread budget: under EP-local routing every consumer pulls from every
            # producer, and with issue-ahead each can park K produce calls in the
            # cache-wait — C*K blocked calls. A smaller pool queues the control
            # plane behind them and deadlocks: the parked produces wait for a
            # publish that can never get a thread (the sync-2 wedge at 235B tp8,
            # 16 parked vs 8 threads). Size for the worst case plus control-plane
            # slack.
            max_concurrency=ii.num_consumers * ii.num_rdt_buffers + 4,
            enable_tensor_transport=True,
            scheduling_strategy=NodeAffinitySchedulingStrategy(
                node_id=node_id, soft=False
            ),
            runtime_env=runtime_env,
        )
        self._server = server_cls.remote(
            served_names=served_names,
            num_rdt_buffers=ii.num_rdt_buffers,
            buffer_presize_gb=ii.buffer_presize_gb,
            gather_lookahead=ii.gather_lookahead,
            stall_timeout_s=ii.stall_timeout_s,
            workers_per_replica=ii.workers_per_replica,
        )
        ray.get(self._server.ping.remote())
        # Pre-barrier NIXL warmup: must complete before the server-name
        # all-gather so no rank can be inside a collective yet (see warmup_nixl).
        ray.get(self._server.warmup_nixl.remote())

    def _build_worker_init_info(self, server_names: list[str]):
        from vllm.distributed.weight_transfer.sharded_rdt_engine import (
            ShardedRDTWeightTransferInitInfo,
        )

        group_lens = [len(g) for g in self._groups]
        names = [m.name for m in self._meta]
        dtype_names = [str(m.dtype).split(".")[-1] for m in self._meta]
        shapes = [list(m.shape) for m in self._meta]
        return ShardedRDTWeightTransferInitInfo(
            trainer_actor_names=server_names,
            trainer_actor_namespace=self._init_info.trainer_actor_namespace,
            produce_method_name=PRODUCE_METHOD_NAME,
            names=names,
            dtype_names=dtype_names,
            shapes=shapes,
            group_lens=group_lens,
            owner_sets=self._owner_sets,
            name_owner_class=self._name_owner_class,
            num_consumers=self._init_info.num_consumers,
            num_rdt_buffers=self._init_info.num_rdt_buffers,
            buffer_presize_gb=self._init_info.buffer_presize_gb,
        )

    # ---------------- per-round ----------------

    def send_weights(self, live_consumer_ids: Collection[int] | None = None) -> None:
        """Gather this rank's weights and publish them for the consumers to pull.

        ``live_consumer_ids`` restricts the sync to the consumers still alive;
        ``None`` serves the whole provisioned set. The provisioned geometry is
        FROZEN for the run — a degraded sync only lowers the live count handed to
        ``begin_sync``. Every rank must get the SAME live set, since they share
        the gather collectives, so the caller computes it once for all of them.
        """
        assert self.source is not None
        if live_consumer_ids is None:
            live_ids = list(range(self._init_info.num_consumers))
            live_count = len(live_ids)
        else:
            live_ids = sorted(set(int(c) for c in live_consumer_ids))
            live_count = len(live_ids)
            logger.warning(
                "[rdt-degraded] serving %d/%d live consumers; every group's "
                "free barrier counts to the live total",
                live_count,
                self._init_info.num_consumers,
            )
        self._send_weights_inner(live_count, live_ids)

    def _send_weights_inner(self, live_count: int, live_ids: list[int]) -> None:
        if not self.is_sender:
            self._run_gather_loop(
                update_future=None, live_count=live_count, live_ids=live_ids
            )
            return

        self.client.start_weight_update()

        from vllm.distributed.weight_transfer.sharded_rdt_engine import (
            ShardedRDTWeightTransferUpdateInfo,
        )

        empty_update = asdict(ShardedRDTWeightTransferUpdateInfo())
        with ThreadPoolExecutor(max_workers=1) as exe:
            # The workers block inside update_weights until they've pulled every
            # group, so it runs concurrently with the gather/publish loop.
            future = exe.submit(self.client.update_weights, empty_update)
            self._run_gather_loop(
                update_future=future, live_count=live_count, live_ids=live_ids
            )
            future.result()  # surface inference-side errors

        self.client.finish_weight_update()

    def _run_gather_loop(
        self, update_future, live_count: int, live_ids: list[int] | None = None
    ) -> None:
        """Gather this rank's weights group-by-group and publish each into the
        server over CUDA IPC. A gathered group is published — serveable —
        immediately; the loop gates BEFORE the next gather while more than
        `gather_lookahead` groups are unfreed (the per-group free barrier: a
        credit releases when every live consumer has signaled the group). So
        the loop self-paces to the consumers' pull rate with at most
        `gather_lookahead + 1` groups resident. Runs on every rank; only the
        sender has an `update_future` to fail fast on."""
        assert self.source is not None  # guaranteed by trainer_init
        # The live IDS ride along with the count: the free barrier needs only
        # the count, but the slot-sharing rendezvous needs to know WHICH
        # consumers are live.
        self._rpc("begin_sync", live_count, live_ids)
        # One generator resume per GROUP: `iter_groups` yields (names, tensors)
        # per owned group in metadata order. Every owner must reach a group in the
        # same order or their shared gather collective mismatches.
        groups = self.source.iter_groups()
        # Publishes fire without an inline ray.get and are harvested this window
        # deep, so the RPC and server-side rebuild overlap the NEXT group's
        # gather/export. The window also surfaces a server-side error at depth 2
        # rather than at end_sync.
        _PUBLISH_WINDOW = 2
        pending_publish: list = []
        # The memory bound: gate BEFORE gathering while more
        # than `bound` groups are unfreed, so at most bound + 1 are resident. The
        # count is exact — `_inflight` gains its entry right after each gather and
        # only wait_freed/end_sync shrinks it, so a group freed server-side but not
        # yet collected still holds its refs and still counts. The publish window
        # drains first, so every resident group is pullable while we wait.
        #
        # 0 is legal and means NO pipelining: the loop waits for group N to be
        # freed by every live consumer before gathering N+1, so exactly one group
        # is resident and the sync costs the full serialized
        # gather + publish + pull + free RTT per group. 1 (the default) overlaps
        # N+1's gather with N's pulls.
        bound = max(0, self._init_info.gather_lookahead)

        # [RDT-EXPORT-RING] `bound + 1` slots is exactly the residency the credit
        # gate enforces. Small storages are packed into a slot and exported once
        # per slot instead of once per storage (518 -> 7 cudaIpcGetMemHandle
        # calls per rank per sync at 235B, each 0.5-1.5ms).
        nslots = bound + 1
        # data_ptr -> (storage weakref, reduce_args), this sync only. `storages`
        # is per-group, so a source that reuses a buffer across groups would
        # otherwise re-export it every group. The weakref is load-bearing: a
        # freed allocation can be reissued at the same address, and serving that
        # from a stale handle is silently wrong bytes.
        handle_cache: dict[int, tuple] = {}
        if len(getattr(self, "_export_ring", [])) != nslots:
            self._export_ring = [None] * nslots
            self._export_ring_args = [None] * nslots
        # Slots are handed out per group and returned by _drop_inflight, so the
        # pool starts full every sync.
        self._slot_of_group = {}
        self._free_slots = list(range(nslots))

        try:
            for gi in self._owned_idx:
                group = self._groups[gi]
                while len(self._inflight) > bound:
                    while pending_publish:
                        self._await_publish(pending_publish.pop(0))
                    self._drop_inflight(self._rpc("wait_freed"))
                names, tensors = next(groups)
                if list(names) != list(group):
                    raise RuntimeError(
                        f"WeightSource group yielded {len(names)} names starting "
                        f"{names[:2]!r} but expected {len(group)} starting "
                        f"{group[:2]!r}; iteration order must match metadata."
                    )
                self._validate_held_yields(gi, names, tensors)
                # Share each unique STORAGE once and describe every name as an
                # as_strided view onto it. ``None`` means a name this rank does not
                # hold (a foreign expert); the source keeps it in the list so the
                # order check stays rank-uniform, and it is dropped here, before
                # the IPC export, matching the sidecar's served_names.
                storages: dict[int, tuple] = {}
                views: dict[str, tuple] = {}
                refs: dict[str, torch.Tensor] = {}
                small: list = []
                for name, tensor in zip(names, tensors):
                    if tensor is None:
                        continue
                    tensor = tensor.detach()
                    if not tensor.is_cuda:
                        tensor = tensor.cuda()
                    # Threshold on the STORAGE, not the tensor: a source may hand
                    # out many small views of one large stacked buffer, and a
                    # per-tensor test would pack the whole thing, costing +1
                    # group of peak memory for a storage already coalesced.
                    if tensor.untyped_storage().nbytes() <= _EXPORT_RING_MAX_BYTES:
                        small.append((name, tensor))
                        continue
                    tensor = tensor.contiguous()
                    refs[name] = tensor  # keep the export alive
                    ust = tensor.untyped_storage()
                    sid = ust.data_ptr()
                    if sid not in storages:
                        cached = handle_cache.get(sid)
                        if cached is not None and not cached[0].expired():
                            reduce_args = cached[1]
                        else:
                            base = torch.empty(
                                0, dtype=torch.uint8, device=tensor.device
                            )
                            base.set_(ust, 0, (ust.nbytes(),))
                            _rebuild, reduce_args = reduce_tensor(base)
                            handle_cache[sid] = (StorageWeakRef(ust), reduce_args)
                        storages[sid] = reduce_args
                    views[name] = (
                        sid,
                        str(tensor.dtype).split(".")[-1],
                        list(tensor.shape),
                        list(tensor.stride()),
                        tensor.storage_offset(),
                    )
                if small:
                    # Take a slot no live group holds. The gate above already
                    # guarantees one exists, so this is a backstop -- but it
                    # must drain publishes first, like the credit gate.
                    while not self._free_slots:
                        while pending_publish:
                            self._await_publish(pending_publish.pop(0))
                        self._drop_inflight(self._rpc("wait_freed"))
                    slot_idx = self._free_slots.pop(0)
                    self._slot_of_group[gi] = slot_idx
                    st, vw, rf = self._pack_group_for_export(small, slot_idx)
                    storages.update(st)
                    views.update(vw)
                    refs.update(rf)
                del small
                del tensors
                # The loop leaves its last `tensor` / `ust` / `base` bound
                # through the credit gate and into the next gather, and `base`
                # is a whole-storage view -- so one tensor and its entire
                # allocation stay alive past `_drop_inflight`, unseen by the
                # `_inflight` accounting the residency bound rests on.
                # Assignment, not `del`: unbound if no direct-path tensor.
                tensor = ust = base = None
                if not refs:
                    # A group with nothing held here cannot occur (every group
                    # carries replicated names), but publishing an empty group
                    # would park a credit nobody's pull is waiting on; skip.
                    continue
                # Hold our refs before publishing; drop them only when the
                # server reports the group freed (IPC export must outlive import).
                self._inflight[gi] = refs
                pending_publish.append(self._publish_async(gi, (storages, views)))
                while len(pending_publish) >= _PUBLISH_WINDOW:
                    self._await_publish(pending_publish.pop(0))
                if update_future is not None and update_future.done():
                    # update_weights returned/failed early — surface now instead
                    # of blocking further gathers.
                    update_future.result()
            while pending_publish:
                self._await_publish(pending_publish.pop(0))
            freed = self._rpc("end_sync")
            self._drop_inflight(freed)
        except BaseException as e:
            with contextlib.suppress(Exception):
                self._rpc("set_gather_error", repr(e))
            self._inflight.clear()
            raise
        finally:
            # Keep the slots, drop the handles: the sidecar's base cache is
            # cleared per sync, so a handle must not outlive the sync either.
            self._export_ring_args = [None] * len(getattr(self, "_export_ring", []))

    def _drop_inflight(self, freed_keys: list) -> None:
        """Release a freed group's refs and return its export-ring slot."""
        free_slots = getattr(self, "_free_slots", None)
        slot_of_group = getattr(self, "_slot_of_group", None)
        for k in freed_keys:
            gi = int(k)
            self._inflight.pop(gi, None)
            if slot_of_group is None:
                continue
            slot = slot_of_group.pop(gi, None)
            if slot is not None and free_slots is not None and slot not in free_slots:
                free_slots.append(slot)

    # ---------------- misc ----------------

    def shutdown(self) -> None:
        if self._server is None:
            return
        import ray

        with contextlib.suppress(Exception):
            ray.get(self._server.shutdown.remote())
            ray.kill(self._server)
        self._server = None
        self._inflight.clear()

_all_gather_owned(world, mine)

All-gather each rank's (metadata digest, held-name bitmask).

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _all_gather_owned(self, world: int, mine: tuple) -> list[tuple]:
    """All-gather each rank's (metadata digest, held-name bitmask)."""
    if world <= 1 or not (
        torch.distributed.is_available() and torch.distributed.is_initialized()
    ):
        return [mine]
    gathered: list[Any] = [None] * world
    torch.distributed.all_gather_object(gathered, mine)
    return gathered

_await_publish(ref)

Resolve one async publish so a server-side rebuild error surfaces at window depth instead of at end_sync. Publishes carry nothing back — freed groups flow only through wait_freed/end_sync (one channel; see publish_group).

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _await_publish(self, ref) -> None:
    """Resolve one async publish so a server-side rebuild error surfaces at
    window depth instead of at end_sync. Publishes carry nothing back —
    freed groups flow only through wait_freed/end_sync (one channel; see
    publish_group)."""
    import ray

    if isinstance(ref, ray.ObjectRef):
        ray.get(ref)

_drop_inflight(freed_keys)

Release a freed group's refs and return its export-ring slot.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _drop_inflight(self, freed_keys: list) -> None:
    """Release a freed group's refs and return its export-ring slot."""
    free_slots = getattr(self, "_free_slots", None)
    slot_of_group = getattr(self, "_slot_of_group", None)
    for k in freed_keys:
        gi = int(k)
        self._inflight.pop(gi, None)
        if slot_of_group is None:
            continue
        slot = slot_of_group.pop(gi, None)
        if slot is not None and free_slots is not None and slot not in free_slots:
            free_slots.append(slot)

_meta_digest()

Stable digest of this rank's metadata (name order + count).

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _meta_digest(self) -> str:
    """Stable digest of this rank's metadata (name order + count)."""
    import hashlib

    h = hashlib.sha256()
    h.update(f"{len(self._meta)}\n".encode())
    for m in self._meta:
        h.update(m.name.encode())
        h.update(b"\n")
    return h.hexdigest()[:16]

_pack_group_for_export(held, slot_idx)

Copy held into ring slot slot_idx; return (storages, views, refs) in the same shape the per-storage path returns, but with ONE storage. Views are built exactly as publish_group rebuilds them, so the two sides cannot disagree about layout.

Slot safety is not the credit gate alone: that bounds how MANY groups are unfreed, not the order they free in, and barriers do complete out of order. The caller takes slot_idx from a pool keyed by live group (_slot_of_group / _free_slots); _drop_inflight returns a slot only once its group is freed everywhere.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _pack_group_for_export(self, held: list, slot_idx: int) -> tuple:
    """Copy ``held`` into ring slot ``slot_idx``; return ``(storages, views,
    refs)`` in the same shape the per-storage path returns, but with ONE
    storage. Views are built exactly as ``publish_group`` rebuilds them, so
    the two sides cannot disagree about layout.

    Slot safety is not the credit gate alone: that bounds how MANY groups
    are unfreed, not the order they free in, and barriers do complete out of
    order. The caller takes ``slot_idx`` from a pool keyed by live group
    (``_slot_of_group`` / ``_free_slots``); ``_drop_inflight`` returns a slot
    only once its group is freed everywhere.
    """
    offsets: list[int] = []
    cur = 0
    for _name, t in held:
        cur = (cur + 15) & ~15  # 16B keeps every element size we ship aligned
        offsets.append(cur)
        cur += t.numel() * t.element_size()
    need = max(16, (cur + 15) & ~15)

    slot = self._export_ring[slot_idx]
    device = held[0][1].device
    if slot is None or slot.numel() < need or slot.device != device:
        self._export_ring[slot_idx] = self._export_ring_args[slot_idx] = None
        slot = torch.empty(need, dtype=torch.uint8, device=device)
        self._export_ring[slot_idx] = slot

    ust = slot.untyped_storage()
    sid = ust.data_ptr()
    reduce_args = self._export_ring_args[slot_idx]
    if reduce_args is None:
        base = torch.empty(0, dtype=torch.uint8, device=device)
        base.set_(ust, 0, (ust.nbytes(),))
        _rebuild, reduce_args = reduce_tensor(base)
        self._export_ring_args[slot_idx] = reduce_args

    views: dict[str, tuple] = {}
    refs: dict[str, torch.Tensor] = {}
    for (name, t), off in zip(held, offsets):
        shape, esz = tuple(t.shape), t.element_size()
        stride = self._contig_stride(shape)
        dst = torch.as_strided(slot.view(t.dtype), shape, stride, off // esz)
        dst.copy_(t)
        refs[name] = dst
        views[name] = (
            sid,
            str(t.dtype).split(".")[-1],
            list(shape),
            list(stride),
            off // esz,
        )
    # The producer sidecar reads this slot from another process over CUDA
    # IPC, while these copies are merely enqueued on this rank's stream;
    # nothing orders the two. Without this sync it packs bytes the copy has
    # not written yet -- silent corruption, and only for ring-packed tensors.
    if slot.is_cuda:
        torch.cuda.current_stream(slot.device).synchronize()
    return {sid: reduce_args}, views, refs

_publish_async(group_idx, entries)

Fire publish_group WITHOUT waiting on the RPC (the gather loop overlaps the publish's server-side rebuild with the next group's gather) and return a handle that _await_publish resolves. Ray actor handle in production; a plain (non-Ray) fake server runs inline.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _publish_async(self, group_idx: int, entries):
    """Fire publish_group WITHOUT waiting on the RPC (the gather loop
    overlaps the publish's server-side rebuild with the next group's
    gather) and return a handle that ``_await_publish`` resolves. Ray actor
    handle in production; a plain (non-Ray) fake server runs inline."""
    method = self._server.publish_group
    remote = getattr(method, "remote", None)
    if remote is not None:
        return remote(group_idx, entries)
    return method(group_idx, entries)

_resolve_ownership(world, rank)

Resolve which rank holds which name, and this rank's publish plan.

A source may hold only part of the model — pipeline stages, expert parallelism, or any mix — so each rank declares its held names and the fleet all-gathers them. The consumers route per name, so the wire carries the transposed result: the distinct owner sets, and a per-name index into them.

The masks are positional over metadata order, which is why the metadata digest is checked first: a rank whose names disagree would transpose into the wrong owners entirely.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _resolve_ownership(self, world: int, rank: int) -> None:
    """Resolve which rank holds which name, and this rank's publish plan.

    A source may hold only part of the model — pipeline stages, expert
    parallelism, or any mix — so each rank declares its held names and the
    fleet all-gathers them. The consumers route per name, so the wire carries
    the transposed result: the distinct owner sets, and a per-name index into
    them.

    The masks are positional over metadata order, which is why the metadata
    digest is checked first: a rank whose names disagree would transpose into
    the wrong owners entirely.
    """
    assert self.source is not None  # guaranteed by trainer_init
    num_groups = len(self._groups)
    names = [m.name for m in self._meta]
    held = self.source.held_names()
    if held is None:
        held_set = set(names)
    else:
        held_set = {str(n) for n in held}
        unknown = sorted(held_set - set(names))
        if unknown:
            raise ValueError(
                f"WeightSource.held_names() lists {len(unknown)} name(s) not "
                f"in metadata(), e.g. {unknown[:3]}."
            )
        if not held_set:
            raise ValueError(
                "WeightSource.held_names() is empty; a rank with nothing to "
                "serve cannot take part in the gather"
            )

    # One collective carries this rank's holdings as a bitmask over metadata
    # order, plus the digest of the metadata it indexes into. The digest is
    # what makes partial ownership safe: only the sender's metadata reaches
    # the consumers, so a rank describing just its own share would silently
    # serve the wrong weights.
    digest = self._meta_digest()
    mask = bytearray((len(names) + 7) // 8)
    for i, n in enumerate(names):
        if n in held_set:
            mask[i >> 3] |= 1 << (i & 7)
    per_rank = self._all_gather_owned(world, (digest, bytes(mask)))
    mismatched = [r for r, (d, *_rest) in enumerate(per_rank) if d != digest]
    if mismatched:
        raise ValueError(
            f"WeightSource.metadata() disagrees across trainer ranks "
            f"(rank {rank} digest {digest}, differing ranks {mismatched[:4]}). "
            "Every rank must describe the WHOLE model, even when it holds "
            "only some of it."
        )
    masks = [m for _d, m in per_rank]

    # Transpose to per-name owners, then dedup into classes. Numbering by
    # FIRST APPEARANCE in metadata order keeps it a pure function of
    # rank-identical inputs, which is what lets a rejoining consumer rebuild
    # the identical table from get_worker_init_payload.
    owner_sets: list[list[int]] = []
    class_of_owners: dict[tuple[int, ...], int] = {}
    name_owner_class: list[int] = []
    for i, n in enumerate(names):
        owners = tuple(r for r, m in enumerate(masks) if m[i >> 3] & (1 << (i & 7)))
        if not owners:
            raise ValueError(
                f"no trainer rank holds {n!r}; every name in metadata() must "
                "be held by at least one rank or it can never be served."
            )
        ci = class_of_owners.get(owners)
        if ci is None:
            ci = len(owner_sets)
            class_of_owners[owners] = ci
            owner_sets.append(list(owners))
        name_owner_class.append(ci)

    self._owner_sets = owner_sets
    self._name_owner_class = name_owner_class
    # Groups holding anything here: exactly what iteration must cover.
    self._owned_idx = [
        gi
        for gi in range(num_groups)
        if any(n in held_set for n in self._groups[gi])
    ]
    self._held_names = held_set

_rpc(method, *args)

Call one of the server actor's methods and block for the result. The single seam through which the engine talks to its server, so tests can inject a local (non-Ray) fake server.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _rpc(self, method: str, *args: Any) -> Any:
    """Call one of the server actor's methods and block for the result.
    The single seam through which the engine talks to its server, so tests
    can inject a local (non-Ray) fake server."""
    import ray

    return ray.get(getattr(self._server, method).remote(*args))

_run_gather_loop(update_future, live_count, live_ids=None)

Gather this rank's weights group-by-group and publish each into the server over CUDA IPC. A gathered group is published — serveable — immediately; the loop gates BEFORE the next gather while more than gather_lookahead groups are unfreed (the per-group free barrier: a credit releases when every live consumer has signaled the group). So the loop self-paces to the consumers' pull rate with at most gather_lookahead + 1 groups resident. Runs on every rank; only the sender has an update_future to fail fast on.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _run_gather_loop(
    self, update_future, live_count: int, live_ids: list[int] | None = None
) -> None:
    """Gather this rank's weights group-by-group and publish each into the
    server over CUDA IPC. A gathered group is published — serveable —
    immediately; the loop gates BEFORE the next gather while more than
    `gather_lookahead` groups are unfreed (the per-group free barrier: a
    credit releases when every live consumer has signaled the group). So
    the loop self-paces to the consumers' pull rate with at most
    `gather_lookahead + 1` groups resident. Runs on every rank; only the
    sender has an `update_future` to fail fast on."""
    assert self.source is not None  # guaranteed by trainer_init
    # The live IDS ride along with the count: the free barrier needs only
    # the count, but the slot-sharing rendezvous needs to know WHICH
    # consumers are live.
    self._rpc("begin_sync", live_count, live_ids)
    # One generator resume per GROUP: `iter_groups` yields (names, tensors)
    # per owned group in metadata order. Every owner must reach a group in the
    # same order or their shared gather collective mismatches.
    groups = self.source.iter_groups()
    # Publishes fire without an inline ray.get and are harvested this window
    # deep, so the RPC and server-side rebuild overlap the NEXT group's
    # gather/export. The window also surfaces a server-side error at depth 2
    # rather than at end_sync.
    _PUBLISH_WINDOW = 2
    pending_publish: list = []
    # The memory bound: gate BEFORE gathering while more
    # than `bound` groups are unfreed, so at most bound + 1 are resident. The
    # count is exact — `_inflight` gains its entry right after each gather and
    # only wait_freed/end_sync shrinks it, so a group freed server-side but not
    # yet collected still holds its refs and still counts. The publish window
    # drains first, so every resident group is pullable while we wait.
    #
    # 0 is legal and means NO pipelining: the loop waits for group N to be
    # freed by every live consumer before gathering N+1, so exactly one group
    # is resident and the sync costs the full serialized
    # gather + publish + pull + free RTT per group. 1 (the default) overlaps
    # N+1's gather with N's pulls.
    bound = max(0, self._init_info.gather_lookahead)

    # [RDT-EXPORT-RING] `bound + 1` slots is exactly the residency the credit
    # gate enforces. Small storages are packed into a slot and exported once
    # per slot instead of once per storage (518 -> 7 cudaIpcGetMemHandle
    # calls per rank per sync at 235B, each 0.5-1.5ms).
    nslots = bound + 1
    # data_ptr -> (storage weakref, reduce_args), this sync only. `storages`
    # is per-group, so a source that reuses a buffer across groups would
    # otherwise re-export it every group. The weakref is load-bearing: a
    # freed allocation can be reissued at the same address, and serving that
    # from a stale handle is silently wrong bytes.
    handle_cache: dict[int, tuple] = {}
    if len(getattr(self, "_export_ring", [])) != nslots:
        self._export_ring = [None] * nslots
        self._export_ring_args = [None] * nslots
    # Slots are handed out per group and returned by _drop_inflight, so the
    # pool starts full every sync.
    self._slot_of_group = {}
    self._free_slots = list(range(nslots))

    try:
        for gi in self._owned_idx:
            group = self._groups[gi]
            while len(self._inflight) > bound:
                while pending_publish:
                    self._await_publish(pending_publish.pop(0))
                self._drop_inflight(self._rpc("wait_freed"))
            names, tensors = next(groups)
            if list(names) != list(group):
                raise RuntimeError(
                    f"WeightSource group yielded {len(names)} names starting "
                    f"{names[:2]!r} but expected {len(group)} starting "
                    f"{group[:2]!r}; iteration order must match metadata."
                )
            self._validate_held_yields(gi, names, tensors)
            # Share each unique STORAGE once and describe every name as an
            # as_strided view onto it. ``None`` means a name this rank does not
            # hold (a foreign expert); the source keeps it in the list so the
            # order check stays rank-uniform, and it is dropped here, before
            # the IPC export, matching the sidecar's served_names.
            storages: dict[int, tuple] = {}
            views: dict[str, tuple] = {}
            refs: dict[str, torch.Tensor] = {}
            small: list = []
            for name, tensor in zip(names, tensors):
                if tensor is None:
                    continue
                tensor = tensor.detach()
                if not tensor.is_cuda:
                    tensor = tensor.cuda()
                # Threshold on the STORAGE, not the tensor: a source may hand
                # out many small views of one large stacked buffer, and a
                # per-tensor test would pack the whole thing, costing +1
                # group of peak memory for a storage already coalesced.
                if tensor.untyped_storage().nbytes() <= _EXPORT_RING_MAX_BYTES:
                    small.append((name, tensor))
                    continue
                tensor = tensor.contiguous()
                refs[name] = tensor  # keep the export alive
                ust = tensor.untyped_storage()
                sid = ust.data_ptr()
                if sid not in storages:
                    cached = handle_cache.get(sid)
                    if cached is not None and not cached[0].expired():
                        reduce_args = cached[1]
                    else:
                        base = torch.empty(
                            0, dtype=torch.uint8, device=tensor.device
                        )
                        base.set_(ust, 0, (ust.nbytes(),))
                        _rebuild, reduce_args = reduce_tensor(base)
                        handle_cache[sid] = (StorageWeakRef(ust), reduce_args)
                    storages[sid] = reduce_args
                views[name] = (
                    sid,
                    str(tensor.dtype).split(".")[-1],
                    list(tensor.shape),
                    list(tensor.stride()),
                    tensor.storage_offset(),
                )
            if small:
                # Take a slot no live group holds. The gate above already
                # guarantees one exists, so this is a backstop -- but it
                # must drain publishes first, like the credit gate.
                while not self._free_slots:
                    while pending_publish:
                        self._await_publish(pending_publish.pop(0))
                    self._drop_inflight(self._rpc("wait_freed"))
                slot_idx = self._free_slots.pop(0)
                self._slot_of_group[gi] = slot_idx
                st, vw, rf = self._pack_group_for_export(small, slot_idx)
                storages.update(st)
                views.update(vw)
                refs.update(rf)
            del small
            del tensors
            # The loop leaves its last `tensor` / `ust` / `base` bound
            # through the credit gate and into the next gather, and `base`
            # is a whole-storage view -- so one tensor and its entire
            # allocation stay alive past `_drop_inflight`, unseen by the
            # `_inflight` accounting the residency bound rests on.
            # Assignment, not `del`: unbound if no direct-path tensor.
            tensor = ust = base = None
            if not refs:
                # A group with nothing held here cannot occur (every group
                # carries replicated names), but publishing an empty group
                # would park a credit nobody's pull is waiting on; skip.
                continue
            # Hold our refs before publishing; drop them only when the
            # server reports the group freed (IPC export must outlive import).
            self._inflight[gi] = refs
            pending_publish.append(self._publish_async(gi, (storages, views)))
            while len(pending_publish) >= _PUBLISH_WINDOW:
                self._await_publish(pending_publish.pop(0))
            if update_future is not None and update_future.done():
                # update_weights returned/failed early — surface now instead
                # of blocking further gathers.
                update_future.result()
        while pending_publish:
            self._await_publish(pending_publish.pop(0))
        freed = self._rpc("end_sync")
        self._drop_inflight(freed)
    except BaseException as e:
        with contextlib.suppress(Exception):
            self._rpc("set_gather_error", repr(e))
        self._inflight.clear()
        raise
    finally:
        # Keep the slots, drop the handles: the sidecar's base cache is
        # cleared per sync, so a handle must not outlive the sync either.
        self._export_ring_args = [None] * len(getattr(self, "_export_ring", []))

_validate_held_yields(gi, names, tensors)

Check held_names() against what the source actually yields — the one place both sit side by side.

Without it, a source that claims a name but yields None for it dies by stall watchdog 300s later: consumers route pulls here, the pull passes the served-names guard, and the cache wait never completes. With it, that is an immediate error naming the weight. One set lookup per name per sync.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _validate_held_yields(self, gi: int, names, tensors) -> None:
    """Check ``held_names()`` against what the source actually yields — the
    one place both sit side by side.

    Without it, a source that claims a name but yields ``None`` for it dies
    by stall watchdog 300s later: consumers route pulls here, the pull
    passes the served-names guard, and the cache wait never completes. With
    it, that is an immediate error naming the weight. One set lookup per
    name per sync."""
    if self._held_names is None:
        return
    for name, tensor in zip(names, tensors):
        if (tensor is None) == (name in self._held_names):
            claim = "does not hold" if tensor is not None else "holds"
            have = "a real tensor" if tensor is not None else "None"
            raise RuntimeError(
                f"held_names() disagrees with the yielded tensors: group {gi} "
                f"name {name!r} yielded {have} but this rank {claim} it. A "
                "WeightSource must yield a real tensor for every held name "
                "and None for the rest (see WeightSource.held_names)."
            )

get_worker_init_payload()

The consumer-side init payload, rebuilt on demand. Pure — no collective.

Reads only retained state, so a restarted inference engine can rejoin at a sync boundary: the driver can ask any time, including mid-run with every rank in its own training step. A collective here would deadlock, since the ranks are not at a matching one.

Raises:

  • RuntimeError

    called before trainer_init cached the server names.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def get_worker_init_payload(self) -> dict:
    """The consumer-side init payload, rebuilt on demand. Pure — no collective.

    Reads only retained state, so a restarted inference engine can rejoin at a
    sync boundary: the driver can ask any time, including mid-run with every
    rank in its own training step. A collective here would deadlock, since the
    ranks are not at a matching one.

    Raises:
        RuntimeError: called before ``trainer_init`` cached the server names.
    """
    if self._server_names is None:
        raise RuntimeError(
            "get_worker_init_payload requires trainer_init to have run (the "
            "producer server names are gathered there)."
        )
    return asdict(self._build_worker_init_info(self._server_names))

send_weights(live_consumer_ids=None)

Gather this rank's weights and publish them for the consumers to pull.

live_consumer_ids restricts the sync to the consumers still alive; None serves the whole provisioned set. The provisioned geometry is FROZEN for the run — a degraded sync only lowers the live count handed to begin_sync. Every rank must get the SAME live set, since they share the gather collectives, so the caller computes it once for all of them.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def send_weights(self, live_consumer_ids: Collection[int] | None = None) -> None:
    """Gather this rank's weights and publish them for the consumers to pull.

    ``live_consumer_ids`` restricts the sync to the consumers still alive;
    ``None`` serves the whole provisioned set. The provisioned geometry is
    FROZEN for the run — a degraded sync only lowers the live count handed to
    ``begin_sync``. Every rank must get the SAME live set, since they share
    the gather collectives, so the caller computes it once for all of them.
    """
    assert self.source is not None
    if live_consumer_ids is None:
        live_ids = list(range(self._init_info.num_consumers))
        live_count = len(live_ids)
    else:
        live_ids = sorted(set(int(c) for c in live_consumer_ids))
        live_count = len(live_ids)
        logger.warning(
            "[rdt-degraded] serving %d/%d live consumers; every group's "
            "free barrier counts to the live total",
            live_count,
            self._init_info.num_consumers,
        )
    self._send_weights_inner(live_count, live_ids)

_RDTProducerServer

Per-rank NIXL serve surface: an internal Ray actor sharing the trainer rank's GPU over CUDA IPC. Holds the gather cache, per-consumer serve rings, the per-group free barrier and the packed serve. The engine feeds it with publish_group; workers pull with rdt_produce_weights_batched and signal with free_group.

A plain class — the engine wraps it with ray.remote(...) at spawn so the actor options live in one place.

Methods:

  • begin_sync

    Reset per-sync free/backpressure state and set this sync's barrier

  • end_sync

    Block until every published group has been freed by its consumers;

  • free_group

    Consumer back-edge: one consumer is done with group group_idx,

  • publish_group

    Rebuild one gather group's CUDA-IPC tensors into the serve cache.

  • rdt_produce_weights_batched

    Serve one batched slice request over NIXL.

  • reserve_serve_buffer

    Pre-allocate + NIXL-register this consumer's serve ring before any

  • set_gather_error

    Record a trainer-side gather failure so blocked serves / publishes

  • wait_freed

    Block until at least one published group has been freed; return and

  • warmup_nixl

    Create this server's NIXL agent now, while the rank's GPU is quiet.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
class _RDTProducerServer:
    """Per-rank NIXL serve surface: an internal Ray actor sharing the trainer
    rank's GPU over CUDA IPC. Holds the gather cache, per-consumer serve rings,
    the per-group free barrier and the packed serve. The engine feeds it with
    ``publish_group``; workers pull with ``rdt_produce_weights_batched`` and
    signal with ``free_group``.

    A plain class — the engine wraps it with ``ray.remote(...)`` at spawn so the
    actor options live in one place.
    """

    def __init__(
        self,
        *,
        num_rdt_buffers: int,
        buffer_presize_gb: float,
        gather_lookahead: int,
        served_names: list[str] | None = None,
        stall_timeout_s: float = DEFAULT_STALL_TIMEOUT_S,
        workers_per_replica: int = 0,
    ) -> None:
        import gc

        self._device_index = torch.accelerator.current_device_index()
        # name -> rebuilt CUDA-IPC tensor (or view); guarded by _cache_cond.
        self._cache: dict[str, torch.Tensor] = {}
        self._cache_cond = threading.Condition()
        self._gather_error: BaseException | None = None

        # Monotonic stamp of the last forward step of the
        # publish -> serve -> free loop. The waits below poll it instead of
        # blocking forever, so a dead consumer fails the sync with a real error.
        self._stall_timeout = float(stall_timeout_s)
        self._last_progress = time.monotonic()

        # Names this producer publishes, so a misrouted pull fails loudly
        # instead of blocking forever in the cache wait. None = serve anything.
        self._served_names = set(served_names) if served_names is not None else None

        # Every live consumer signals free_group(gi) at every
        # owner, once per sync; the group frees when the count reaches
        # begin_sync's live total — one uniform target, no routed per-producer
        # ones. Signals may precede the publish, which completes them.
        self._live_count = 1
        self._free_counts: dict[int, int] = {}
        # gi -> the names this producer published for it (what release drops).
        self._group_names: dict[int, list[str]] = {}

        # Published-but-unfreed groups, plus freed ones not
        # yet handed back. The memory gate lives in the ENGINE's gather loop; this
        # side only accounts — free_group moves a group from _inflight_groups to
        # _freed_pending, which the engine collects via wait_freed / end_sync to
        # drop its storage refs. _lookahead is engine-enforced, unused here.
        self._lookahead = max(0, gather_lookahead)
        self._inflight_groups: list[int] = []
        self._freed_pending: list[int] = []

        # Ring of packed serve buffers, one ring per SLOT-SHARING GROUP (one
        # consumer per group unless several deployments share); the slot within
        # it is the consumer's ``seq % nring``.
        self._nring = max(1, num_rdt_buffers)
        self._serve_rings: dict[int, list[torch.Tensor | None]] = {}
        self._serve_lock = threading.Lock()
        # registerMem on a shared NIXL agent is not concurrency-safe; serialize.
        self._reg_lock = threading.Lock()
        self._buffer_presize = int(buffer_presize_gb * (1 << 30))
        self._serve_device = torch.device("cuda", self._device_index)

        # Slot-sharing state, all guarded by _cache_cond, so the rendezvous
        # waits ride the stall watchdog like every other wait here.
        # ``_share_width`` = consumers per deployment; 0 means every group is a
        # singleton and this degenerates to the unshared serve path.
        self._share_width = max(0, int(workers_per_replica))
        self._live_ids: set[int] | None = None
        self._sharing_active = False
        self._sharers: dict[int, frozenset] = {}  # group -> live members
        self._gens: dict[int, dict] = {}  # group -> {seq: _SharedPack}
        # group -> {cid: plan digest}. Init-time state, not cleared per sync: it
        # is what proves the sharers of a group pull the same chunks.
        self._plan_digests: dict[int, dict[int, str]] = {}

        # (sharing group, ring idx, packed layout) ->
        # (buffer data_ptr, destination views). Keyed on the buffer pointer too, so
        # a ring regrow invalidates rather than writing into a freed buffer. See
        # the serve path for why the layout, not the spec names, is the key.
        self._pack_dsts: dict[tuple, tuple[int, list[torch.Tensor]]] = {}
        # [RDT-EXPORT-RING] handle -> rebuilt base. The trainer reuses buffers
        # across groups, so the same handle arrives on many publishes; without
        # this we reopen it every group. Cleared per sync so no IPC mapping into
        # trainer memory outlives the sync that made it.
        self._base_cache: dict[tuple, torch.Tensor] = {}

        # Freeze the static post-init object graph so gen-2 GC never stops the
        # world mid-serve, which shows up as serve stragglers.
        gc.collect()
        gc.freeze()

    def ping(self) -> int:
        return self._device_index

    # ---------------- stall watchdog ----------------

    def _note_progress_locked(self) -> None:
        """Record that the credit loop moved. Caller holds ``_cache_cond``."""
        self._last_progress = time.monotonic()

    def _wait_for(self, blocked: Callable[[], bool], what: str) -> None:
        """``_cache_cond.wait()`` with a liveness bound.

        Waits while ``blocked()`` holds, returning early on a gather error. If
        nothing on this producer progresses for ``_stall_timeout``, self-fires
        ``set_gather_error``, which every waiter here already checks — so the rank
        unwinds through one path and the driver gets a real exception.

        The progress stamp is global to the producer, not per-waiter: a merely
        slow waiter is kept alive by its peers' progress. Caller holds
        ``_cache_cond``.
        """
        while blocked():
            if self._gather_error is not None:
                return
            self._cache_cond.wait(_STALL_POLL_S)
            if not blocked() or self._gather_error is not None:
                return
            stalled = time.monotonic() - self._last_progress
            if stalled >= self._stall_timeout:
                msg = (
                    f"RDT stall: no progress for {stalled:.0f}s while waiting for "
                    f"{what} (timeout {self._stall_timeout:.0f}s). A consumer most "
                    f"likely died mid-sync: {len(self._inflight_groups)} group(s) "
                    f"published and unfreed."
                )
                logger.error("[rdt-stall] %s", msg)
                # Same channel a gather failure uses: wakes every waiter here, and
                # each raises rather than returning a half-served result.
                self._gather_error = RuntimeError(msg)
                self._cache_cond.notify_all()
                return

    def warmup_nixl(self) -> None:
        """Create this server's NIXL agent now, while the rank's GPU is quiet.

        Called at spawn, before the server-name all-gather, so no rank can be
        spinning in a collective. Creating the agent lazily instead deadlocks on
        EFA-class fabrics (see "warmup_nixl breaks a startup deadlock" in the
        doc). The warmup buffer stays registered so the agent's CUDA-HMEM path
        stays initialized.
        """
        from ray.experimental import register_nixl_memory

        self._nixl_warmup_buf = torch.zeros(1 << 20, dtype=torch.uint8, device="cuda")
        with self._reg_lock:
            register_nixl_memory(self._nixl_warmup_buf)

    # ---------------- engine-facing (per sync) ----------------

    def begin_sync(
        self, live_count: int, live_consumer_ids: list | None = None
    ) -> None:
        """Reset per-sync free/backpressure state and set this sync's barrier
        target.

        ``live_count`` is how many consumers take part in THIS sync. Required, no
        default: a forgotten argument silently targeting 1 would free groups after
        the FIRST signal while others still pull — use-after-free, not an error.
        The driver awaits the previous sync's finish before the next begins, so
        nothing is in flight; a straggler signal would otherwise credit the wrong
        sync, which is why the consumer drains its signals before finishing.

        ``live_consumer_ids`` is that same live set enumerated, which is what
        sizes each slot-sharing group's rendezvous; a count cannot, since a group
        is a specific set of ids. ``None`` leaves every group a singleton.

        The packed-destination cache deliberately survives: the layout repeats
        every sync. So does ``_plan_digests``, which is init-time state.
        """
        with self._cache_cond:
            self._gather_error = None
            self._live_count = max(1, int(live_count))
            self._live_ids = (
                set(int(c) for c in live_consumer_ids)
                if live_consumer_ids is not None
                else None
            )
            # Sharing is on only when the geometry shows several deployments;
            # one deployment takes the singleton path.
            self._sharing_active = (
                self._share_width > 0
                and self._live_ids is not None
                and len(self._live_ids) > self._share_width
            )
            self._sharers.clear()
            self._gens.clear()
            self._inflight_groups.clear()
            self._freed_pending.clear()
            self._free_counts.clear()
            self._group_names.clear()
            self._base_cache.clear()
            self._note_progress_locked()

    def _release_group_locked(self, group_idx: int) -> None:
        """Drop a freed group's cache entries and queue it for the engine
        (whose gather-credit gate blocks in ``wait_freed`` on exactly this).

        Shared by the last ``free_group`` and by ``publish_group`` completing an
        early-signaled group. Caller must hold ``_cache_cond``.
        """
        for name in self._group_names.pop(group_idx, ()):
            self._cache.pop(name, None)
        self._free_counts.pop(group_idx, None)
        if group_idx in self._inflight_groups:
            self._inflight_groups.remove(group_idx)
            self._freed_pending.append(group_idx)

    def publish_group(self, group_idx: int, entries: tuple) -> None:
        """Rebuild one gather group's CUDA-IPC tensors into the serve cache.

        NEVER blocks: a gathered group is serveable immediately. The memory bound
        lives in the engine's gather loop, which stops GATHERING (not publishing)
        past ``gather_lookahead`` unfreed groups.

        ``entries`` is ``(storages, views)``: one CUDA-IPC export per storage,
        plus per-name ``(sid, dtype_name, shape, stride, storage_offset)`` rebuilt
        here as ``as_strided`` views.

        Signals can arrive BEFORE their publish — a consumer pulling nothing of a
        group signals it as its plan starts — so a group whose barrier is already
        satisfied is released here. Freed groups reach the engine only through
        ``wait_freed`` / ``end_sync``: a freed notice riding an unharvested async
        publish result would wedge the loop.
        """
        storages, views = entries
        bases: dict[int, torch.Tensor] = {}
        for sid, reduce_args in storages.items():
            cached = self._base_cache.get(reduce_args)
            if cached is not None:
                bases[sid] = cached
                continue
            list_args = list(reduce_args)
            # Index 6 of reduce_tensor's args is the exporter's device index;
            # rebuild on this server's device (same physical GPU as the rank).
            list_args[6] = self._device_index
            bases[sid] = self._base_cache[reduce_args] = rebuild_cuda_tensor(*list_args)
        rebuilt: dict[str, torch.Tensor] = {}
        for name, (sid, dtype_name, shape, stride, storage_offset) in views.items():
            typed = bases[sid].view(getattr(torch, dtype_name))
            rebuilt[name] = torch.as_strided(typed, shape, stride, storage_offset)
        del bases

        with self._cache_cond:
            self._cache.update(rebuilt)
            self._inflight_groups.append(group_idx)
            self._group_names[group_idx] = list(rebuilt)
            if self._free_counts.get(group_idx, 0) >= self._live_count:
                self._release_group_locked(group_idx)
            self._note_progress_locked()
            self._cache_cond.notify_all()

    def wait_freed(self) -> list[int]:
        """Block until at least one published group has been freed; return and
        clear the freed backlog. The engine's gather-credit gate calls this once
        its loop is ``gather_lookahead`` groups ahead, so this wait is where the
        trainer paces to the consumers' pull rate.

        Raises rather than returning empty when the sync errors or stalls: the
        engine is blocked here inside its gather loop, and an empty return would
        spin it straight back in.
        """
        with self._cache_cond:
            self._wait_for(lambda: not self._freed_pending, "a freed-group credit")
            if not self._freed_pending:
                raise RuntimeError(
                    "gather errored while waiting for a freed-group credit: "
                    f"{self._gather_error!r}"
                )
            freed = self._freed_pending
            self._freed_pending = []
            return freed

    def end_sync(self) -> list[int]:
        """Block until every published group has been freed by its consumers;
        return the remaining freed keys so the engine drops its last refs."""
        with self._cache_cond:
            self._wait_for(
                lambda: bool(self._inflight_groups),
                "every published group to be freed",
            )
            freed = self._freed_pending
            self._freed_pending = []
            self._base_cache.clear()
            return freed

    def set_gather_error(self, message: str) -> None:
        """Record a trainer-side gather failure so blocked serves / publishes
        stop waiting and surface it."""
        with self._cache_cond:
            self._gather_error = RuntimeError(message)
            self._cache_cond.notify_all()

    # ---------------- consumer-facing (called by name over Ray) ----------------

    def free_group(self, group_idx: int) -> None:
        """Consumer back-edge: one consumer is done with group ``group_idx``,
        either because its last chunk landed or because it had nothing to pull.

        The per-group barrier counts one signal per live consumer against the
        ``begin_sync`` count; every consumer signals every owner, so the target is
        the same integer everywhere. The last signal drops the cache entries and
        queues the group as gather credit for ``wait_freed``. A signal arriving
        before its publish is only counted — ``publish_group`` completes it.
        """
        gi = int(group_idx)
        with self._cache_cond:
            count = self._free_counts.get(gi, 0) + 1
            self._free_counts[gi] = count
            if gi in self._group_names and count >= self._live_count:
                self._release_group_locked(gi)
            self._note_progress_locked()
            self._cache_cond.notify_all()

    def _new_serve_buffer(self, nbytes: int) -> torch.Tensor:
        """Allocate + NIXL-register one serve slot: the single allocation seam,
        so registration cannot be skipped on either of the two paths that make
        buffers (the init-time reservation and the serve-path backstop)."""
        from ray.experimental import register_nixl_memory

        t = torch.empty(nbytes, dtype=torch.uint8, device=self._serve_device)
        with self._reg_lock:
            register_nixl_memory(t)
        return t

    def reserve_serve_buffer(
        self, consumer_id: int, nbytes: int, plan_digest: str | None = None
    ) -> None:
        """Pre-allocate + NIXL-register this consumer's serve ring before any
        pull, while the fabric is idle (avoids registration races during the
        sync-0 RDMA churn under M:N fan-in). Idempotent; grows only if needed.

        The ring is keyed by SLOT-SHARING GROUP, so the sharers of one group
        reserve ONE ring between them. They all call this, same group and same
        size, so the whole body runs under ``_serve_lock``, or two concurrent
        first-callers would each allocate a ring and one would be dropped while
        still registered.

        ``plan_digest`` is the caller's ordered chunk plan for THIS producer.
        Sharers must agree on it, and this is where a disagreement is caught
        rather than at serve time, where it is a rendezvous nobody completes.

        Raises:
            RuntimeError: two consumers of one sharing group disagree on the
                chunks they pull from this producer.
        """
        sg = self._share_group(consumer_id)
        if plan_digest is not None:
            with self._cache_cond:
                seen = self._plan_digests.setdefault(sg, {})
                clash = next(
                    ((c, d) for c, d in seen.items() if d != plan_digest), None
                )
                if clash is None:
                    seen[consumer_id] = plan_digest
            if clash is not None:
                raise RuntimeError(
                    f"consumers {clash[0]} and {consumer_id} are in slot-sharing "
                    f"group {sg} but do not pull the same chunks from this "
                    f"producer ({clash[1]} != {plan_digest}). Sharing a serve "
                    f"slot requires the deployments to be identical; check that "
                    f"workers_per_replica matches the inference fleet's "
                    f"geometry."
                )
        # This is where the producer's NIXL serve memory is committed: one ring
        # of K slots per SHARING GROUP that pulls from this rank -- with a single
        # deployment, one ring per consumer, since every group is a singleton.
        # Routing only sends a consumer here for names this rank owns, so the
        # count is the fan-in, not the fleet.
        #
        # Every slot is sized by the LARGEST chunk that consumer pulls from us,
        # rounded up to a 256 MB multiple, so the whole ring pays for the worst
        # chunk in the plan while most chunks are far smaller.
        #
        # TODO: that over-allocates. Sizing slots per chunk instead of per plan,
        # or capping chunk bytes so no chunk is an outlier, would shrink it; so
        # would dropping the 256 MB round-up, which is slack once a buffer never
        # regrows. Row-splitting the vocab matrix shrinks the outlier itself.
        # The host-staging TODO above would remove these rings altogether.
        alloc = buffer_alloc_bytes(nbytes, self._buffer_presize)
        with self._serve_lock:
            rings = self._serve_rings.setdefault(sg, [None] * self._nring)
            for i in range(self._nring):
                slot = rings[i]
                if slot is None or slot.numel() < alloc:
                    rings[i] = self._new_serve_buffer(alloc)

    def _share_group(self, consumer_id: int) -> int:
        """The slot-sharing group ``consumer_id`` belongs to: its index within
        its own deployment, since that is what fixes the plan. Without a width
        the group is the consumer itself and nothing is shared."""
        return consumer_id % self._share_width if self._share_width > 0 else consumer_id

    def _sharers_of(self, sg: int) -> frozenset:
        """The LIVE consumers of group ``sg``: the rendezvous width. Derived from
        ``begin_sync``'s live set, so a degraded sync narrows the barrier instead
        of waiting forever on a dead deployment. Caller holds ``_cache_cond``."""
        cached = self._sharers.get(sg)
        if cached is None:
            if not self._sharing_active or self._live_ids is None:
                cached = frozenset((sg,))
            else:
                cached = frozenset(
                    c for c in self._live_ids if c % self._share_width == sg
                ) or frozenset((sg,))
            self._sharers[sg] = cached
        return cached

    def _join_share_group(self, consumer_id: int, seq: int) -> tuple:
        """Register this call's arrival in its group's rendezvous for ``seq`` and
        return ``(generation, is_packer)``.

        Exactly one caller per generation gets ``is_packer=True``, whichever
        completes the arrival set, and it is the only one that touches the GPU.
        The slot is ``seq % nring``: fixed by the consumers' issue order, so it
        needs no release accounting on this side.
        """
        sg = self._share_group(consumer_id)
        with self._cache_cond:
            gens = self._gens.setdefault(sg, {})
            gen = gens.get(seq)
            if gen is None:
                gen = gens[seq] = _SharedPack(group=sg, slot=seq % self._nring)
            gen.arrived.add(consumer_id)
            if gen.packing or gen.done or not gen.arrived >= self._sharers_of(sg):
                return gen, False
            gen.packing = True
            return gen, True

    def _await_shared_pack(self, gen: _SharedPack) -> torch.Tensor:
        """Block until this generation's packer published its blob, and return
        it. A failed pack is re-raised here, so every sharer of a bad pack fails
        instead of reading a half-written slot."""
        with self._cache_cond:
            self._wait_for(
                lambda: not gen.done and gen.error is None,
                "a co-replica's shared serve pack",
            )
            if gen.error is not None:
                raise RuntimeError(
                    f"the sharer packing this chunk failed: {gen.error!r}"
                ) from gen.error
            if not gen.done:
                raise RuntimeError(
                    f"gather errored while awaiting a shared pack: "
                    f"{self._gather_error!r}"
                )
            return gen.blob

    def _serve_slot(self, sg: int, idx: int, need: int) -> torch.Tensor:
        """Group ``sg``'s ring slot ``idx``, grown if this chunk outgrew the
        reservation. Growing registers memory while the fabric is busy, the
        hazard ``reserve_serve_buffer`` exists to avoid, so it is a backstop: the
        reservation is sized from the same static plan as this pack."""
        with self._serve_lock:
            buffer = self._serve_rings.setdefault(sg, [None] * self._nring)[idx]
        if buffer is not None and buffer.numel() >= need:
            return buffer
        buffer = self._new_serve_buffer(buffer_alloc_bytes(need, self._buffer_presize))
        with self._serve_lock:
            self._serve_rings.setdefault(sg, [None] * self._nring)[idx] = buffer
        return buffer

    def _fail_shared_pack(self, gen: _SharedPack, exc: BaseException) -> None:
        """Publish a pack failure so waiting sharers raise instead of hanging on
        a generation that will never complete."""
        with self._cache_cond:
            gen.error = exc
            self._cache_cond.notify_all()

    @ray.method(tensor_transport="nixl")
    def rdt_produce_weights_batched(
        self, specs: list, consumer_id: int = 0, seq: int = -1
    ):
        """Serve one batched slice request over NIXL.

        Waits until the specs' names are cached, then rendezvouses with the
        other live sharers of this consumer's slot-sharing group. The last to
        arrive replays each spec's op chain (pure views into cached tensors,
        guarded by ALLOWED_OPS) and byte-packs the slices 16B-aligned into the
        group's ring slot ``seq % nring``, mirroring the consumer's identical
        layout; every sharer then returns that one packed blob, so R deployments
        cost one pack and one slot instead of R. With a single deployment every
        group is a singleton, so the arriving call is always its own packer and
        this is the unshared serve path exactly.

        ``seq`` is the caller's index in its pull stream to this producer, which
        is what fixes the slot. Callers that want a single slice pass one spec
        and read the blob back with that slice's dtype/shape.

        Raises:
            ValueError: ``seq`` was not supplied.
        """
        needed = sorted({n for n, _ in specs})
        if self._served_names is not None:
            unserved = [n for n in needed if n not in self._served_names]
            if unserved:
                # Without this the cache wait below would block forever: this
                # producer never gathers these names, so they can never arrive.
                raise RuntimeError(
                    f"pull routed to the wrong producer: {unserved[:3]} "
                    f"({len(unserved)} names) are not served here"
                )
        with self._cache_cond:
            self._wait_for(
                lambda: not all(n in self._cache for n in needed),
                f"{len(needed)} name(s) to be published",
            )
            if not all(n in self._cache for n in needed):
                # _wait_for only gives up on a gather error (its own stall
                # included), so the names are never coming.
                raise RuntimeError(
                    f"gather errored before {needed}: {self._gather_error!r}"
                )

        if seq < 0:
            # The slot is derived from it, so a caller that does not send one
            # cannot be served safely.
            raise ValueError(
                "rdt_produce_weights_batched requires the consumer's issue index `seq`"
            )
        gen, is_packer = self._join_share_group(consumer_id, seq)
        if not is_packer:
            blob = self._await_shared_pack(gen)
        else:
            try:
                blob = self._pack_shared(gen, specs)
            except BaseException as e:
                self._fail_shared_pack(gen, e)
                raise
        # A served pull is the third of the four progress signals the stall
        # watchdog reads (publish / produce / free / begin_sync): a long sync whose
        # consumers pull steadily but slowly must never trip it.
        with self._cache_cond:
            self._note_progress_locked()
        return [blob]

    def _pack_shared(self, gen: _SharedPack, specs: list) -> torch.Tensor:
        """Replay the op chains into this generation's slot and publish the blob
        to the sharers waiting on it.

        Runs on exactly one call per generation and OUTSIDE ``_cache_cond``: the
        pack is GPU work and must not block publishes, frees or other groups.
        """
        sliced: list = []  # (byte_off, tensor)
        pack_cur = 0
        for name, chain in specs:
            t = self._cache[name]
            for op, args, kw in chain:
                if op not in ALLOWED_OPS:
                    raise ValueError(f"{name!r}: disallowed op {op!r}")
                t = getattr(t, op)(*args, **dict(kw))
            off = (pack_cur + 15) & ~15
            pack_cur = off + t.numel() * t.element_size()
            sliced.append((off, t))

        buffer = self._serve_slot(gen.group, gen.slot, pack_cur)

        # The destination views are a pure function of the packed
        # layout, which is byte-identical every sync, so build them once per
        # (sharing group, ring slot, layout) and reuse — rebuilding per call cost
        # 5.2ms of a 7.5ms 384-spec 235B group.
        #
        # The key is the LAYOUT, not the spec names: a name can appear in two
        # requests with different op chains (one name's copies can split across
        # owner-class chunks), and serving the second through the first's views
        # would write the wrong bytes with nothing downstream to catch it.
        dst_key = (
            gen.group,
            gen.slot,
            tuple((off, t.dtype, t.shape) for off, t in sliced),
        )
        cached = self._pack_dsts.get(dst_key)
        if cached is None or cached[0] != buffer.data_ptr():
            dsts = []
            for off, t in sliced:
                nb = t.numel() * t.element_size()
                dsts.append(buffer[off : off + nb].view(t.dtype).reshape(t.shape))
            self._pack_dsts[dst_key] = (buffer.data_ptr(), dsts)
        else:
            dsts = cached[1]
        torch._foreach_copy_(dsts, [t for _off, t in sliced])

        blob = buffer[:pack_cur]
        with self._cache_cond:
            gen.blob = blob
            gen.done = True
            self._note_progress_locked()
            self._cache_cond.notify_all()
        return blob

    def shutdown(self) -> None:
        with self._cache_cond:
            self._cache.clear()
        with self._cache_cond:
            # Generations hold blob views into the rings, so they go first.
            self._gens.clear()
            self._sharers.clear()
        with self._serve_lock:
            self._serve_rings.clear()
            # Must go with the rings: these are views INTO them, and the
            # data_ptr guard that normally invalidates them cannot tell a freed
            # buffer from a new one recycled at the same address.
            self._pack_dsts.clear()

_await_shared_pack(gen)

Block until this generation's packer published its blob, and return it. A failed pack is re-raised here, so every sharer of a bad pack fails instead of reading a half-written slot.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _await_shared_pack(self, gen: _SharedPack) -> torch.Tensor:
    """Block until this generation's packer published its blob, and return
    it. A failed pack is re-raised here, so every sharer of a bad pack fails
    instead of reading a half-written slot."""
    with self._cache_cond:
        self._wait_for(
            lambda: not gen.done and gen.error is None,
            "a co-replica's shared serve pack",
        )
        if gen.error is not None:
            raise RuntimeError(
                f"the sharer packing this chunk failed: {gen.error!r}"
            ) from gen.error
        if not gen.done:
            raise RuntimeError(
                f"gather errored while awaiting a shared pack: "
                f"{self._gather_error!r}"
            )
        return gen.blob

_fail_shared_pack(gen, exc)

Publish a pack failure so waiting sharers raise instead of hanging on a generation that will never complete.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _fail_shared_pack(self, gen: _SharedPack, exc: BaseException) -> None:
    """Publish a pack failure so waiting sharers raise instead of hanging on
    a generation that will never complete."""
    with self._cache_cond:
        gen.error = exc
        self._cache_cond.notify_all()

_join_share_group(consumer_id, seq)

Register this call's arrival in its group's rendezvous for seq and return (generation, is_packer).

Exactly one caller per generation gets is_packer=True, whichever completes the arrival set, and it is the only one that touches the GPU. The slot is seq % nring: fixed by the consumers' issue order, so it needs no release accounting on this side.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _join_share_group(self, consumer_id: int, seq: int) -> tuple:
    """Register this call's arrival in its group's rendezvous for ``seq`` and
    return ``(generation, is_packer)``.

    Exactly one caller per generation gets ``is_packer=True``, whichever
    completes the arrival set, and it is the only one that touches the GPU.
    The slot is ``seq % nring``: fixed by the consumers' issue order, so it
    needs no release accounting on this side.
    """
    sg = self._share_group(consumer_id)
    with self._cache_cond:
        gens = self._gens.setdefault(sg, {})
        gen = gens.get(seq)
        if gen is None:
            gen = gens[seq] = _SharedPack(group=sg, slot=seq % self._nring)
        gen.arrived.add(consumer_id)
        if gen.packing or gen.done or not gen.arrived >= self._sharers_of(sg):
            return gen, False
        gen.packing = True
        return gen, True

_new_serve_buffer(nbytes)

Allocate + NIXL-register one serve slot: the single allocation seam, so registration cannot be skipped on either of the two paths that make buffers (the init-time reservation and the serve-path backstop).

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _new_serve_buffer(self, nbytes: int) -> torch.Tensor:
    """Allocate + NIXL-register one serve slot: the single allocation seam,
    so registration cannot be skipped on either of the two paths that make
    buffers (the init-time reservation and the serve-path backstop)."""
    from ray.experimental import register_nixl_memory

    t = torch.empty(nbytes, dtype=torch.uint8, device=self._serve_device)
    with self._reg_lock:
        register_nixl_memory(t)
    return t

_note_progress_locked()

Record that the credit loop moved. Caller holds _cache_cond.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _note_progress_locked(self) -> None:
    """Record that the credit loop moved. Caller holds ``_cache_cond``."""
    self._last_progress = time.monotonic()

_pack_shared(gen, specs)

Replay the op chains into this generation's slot and publish the blob to the sharers waiting on it.

Runs on exactly one call per generation and OUTSIDE _cache_cond: the pack is GPU work and must not block publishes, frees or other groups.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _pack_shared(self, gen: _SharedPack, specs: list) -> torch.Tensor:
    """Replay the op chains into this generation's slot and publish the blob
    to the sharers waiting on it.

    Runs on exactly one call per generation and OUTSIDE ``_cache_cond``: the
    pack is GPU work and must not block publishes, frees or other groups.
    """
    sliced: list = []  # (byte_off, tensor)
    pack_cur = 0
    for name, chain in specs:
        t = self._cache[name]
        for op, args, kw in chain:
            if op not in ALLOWED_OPS:
                raise ValueError(f"{name!r}: disallowed op {op!r}")
            t = getattr(t, op)(*args, **dict(kw))
        off = (pack_cur + 15) & ~15
        pack_cur = off + t.numel() * t.element_size()
        sliced.append((off, t))

    buffer = self._serve_slot(gen.group, gen.slot, pack_cur)

    # The destination views are a pure function of the packed
    # layout, which is byte-identical every sync, so build them once per
    # (sharing group, ring slot, layout) and reuse — rebuilding per call cost
    # 5.2ms of a 7.5ms 384-spec 235B group.
    #
    # The key is the LAYOUT, not the spec names: a name can appear in two
    # requests with different op chains (one name's copies can split across
    # owner-class chunks), and serving the second through the first's views
    # would write the wrong bytes with nothing downstream to catch it.
    dst_key = (
        gen.group,
        gen.slot,
        tuple((off, t.dtype, t.shape) for off, t in sliced),
    )
    cached = self._pack_dsts.get(dst_key)
    if cached is None or cached[0] != buffer.data_ptr():
        dsts = []
        for off, t in sliced:
            nb = t.numel() * t.element_size()
            dsts.append(buffer[off : off + nb].view(t.dtype).reshape(t.shape))
        self._pack_dsts[dst_key] = (buffer.data_ptr(), dsts)
    else:
        dsts = cached[1]
    torch._foreach_copy_(dsts, [t for _off, t in sliced])

    blob = buffer[:pack_cur]
    with self._cache_cond:
        gen.blob = blob
        gen.done = True
        self._note_progress_locked()
        self._cache_cond.notify_all()
    return blob

_release_group_locked(group_idx)

Drop a freed group's cache entries and queue it for the engine (whose gather-credit gate blocks in wait_freed on exactly this).

Shared by the last free_group and by publish_group completing an early-signaled group. Caller must hold _cache_cond.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _release_group_locked(self, group_idx: int) -> None:
    """Drop a freed group's cache entries and queue it for the engine
    (whose gather-credit gate blocks in ``wait_freed`` on exactly this).

    Shared by the last ``free_group`` and by ``publish_group`` completing an
    early-signaled group. Caller must hold ``_cache_cond``.
    """
    for name in self._group_names.pop(group_idx, ()):
        self._cache.pop(name, None)
    self._free_counts.pop(group_idx, None)
    if group_idx in self._inflight_groups:
        self._inflight_groups.remove(group_idx)
        self._freed_pending.append(group_idx)

_serve_slot(sg, idx, need)

Group sg's ring slot idx, grown if this chunk outgrew the reservation. Growing registers memory while the fabric is busy, the hazard reserve_serve_buffer exists to avoid, so it is a backstop: the reservation is sized from the same static plan as this pack.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _serve_slot(self, sg: int, idx: int, need: int) -> torch.Tensor:
    """Group ``sg``'s ring slot ``idx``, grown if this chunk outgrew the
    reservation. Growing registers memory while the fabric is busy, the
    hazard ``reserve_serve_buffer`` exists to avoid, so it is a backstop: the
    reservation is sized from the same static plan as this pack."""
    with self._serve_lock:
        buffer = self._serve_rings.setdefault(sg, [None] * self._nring)[idx]
    if buffer is not None and buffer.numel() >= need:
        return buffer
    buffer = self._new_serve_buffer(buffer_alloc_bytes(need, self._buffer_presize))
    with self._serve_lock:
        self._serve_rings.setdefault(sg, [None] * self._nring)[idx] = buffer
    return buffer

_share_group(consumer_id)

The slot-sharing group consumer_id belongs to: its index within its own deployment, since that is what fixes the plan. Without a width the group is the consumer itself and nothing is shared.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _share_group(self, consumer_id: int) -> int:
    """The slot-sharing group ``consumer_id`` belongs to: its index within
    its own deployment, since that is what fixes the plan. Without a width
    the group is the consumer itself and nothing is shared."""
    return consumer_id % self._share_width if self._share_width > 0 else consumer_id

_sharers_of(sg)

The LIVE consumers of group sg: the rendezvous width. Derived from begin_sync's live set, so a degraded sync narrows the barrier instead of waiting forever on a dead deployment. Caller holds _cache_cond.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _sharers_of(self, sg: int) -> frozenset:
    """The LIVE consumers of group ``sg``: the rendezvous width. Derived from
    ``begin_sync``'s live set, so a degraded sync narrows the barrier instead
    of waiting forever on a dead deployment. Caller holds ``_cache_cond``."""
    cached = self._sharers.get(sg)
    if cached is None:
        if not self._sharing_active or self._live_ids is None:
            cached = frozenset((sg,))
        else:
            cached = frozenset(
                c for c in self._live_ids if c % self._share_width == sg
            ) or frozenset((sg,))
        self._sharers[sg] = cached
    return cached

_wait_for(blocked, what)

_cache_cond.wait() with a liveness bound.

Waits while blocked() holds, returning early on a gather error. If nothing on this producer progresses for _stall_timeout, self-fires set_gather_error, which every waiter here already checks — so the rank unwinds through one path and the driver gets a real exception.

The progress stamp is global to the producer, not per-waiter: a merely slow waiter is kept alive by its peers' progress. Caller holds _cache_cond.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def _wait_for(self, blocked: Callable[[], bool], what: str) -> None:
    """``_cache_cond.wait()`` with a liveness bound.

    Waits while ``blocked()`` holds, returning early on a gather error. If
    nothing on this producer progresses for ``_stall_timeout``, self-fires
    ``set_gather_error``, which every waiter here already checks — so the rank
    unwinds through one path and the driver gets a real exception.

    The progress stamp is global to the producer, not per-waiter: a merely
    slow waiter is kept alive by its peers' progress. Caller holds
    ``_cache_cond``.
    """
    while blocked():
        if self._gather_error is not None:
            return
        self._cache_cond.wait(_STALL_POLL_S)
        if not blocked() or self._gather_error is not None:
            return
        stalled = time.monotonic() - self._last_progress
        if stalled >= self._stall_timeout:
            msg = (
                f"RDT stall: no progress for {stalled:.0f}s while waiting for "
                f"{what} (timeout {self._stall_timeout:.0f}s). A consumer most "
                f"likely died mid-sync: {len(self._inflight_groups)} group(s) "
                f"published and unfreed."
            )
            logger.error("[rdt-stall] %s", msg)
            # Same channel a gather failure uses: wakes every waiter here, and
            # each raises rather than returning a half-served result.
            self._gather_error = RuntimeError(msg)
            self._cache_cond.notify_all()
            return

begin_sync(live_count, live_consumer_ids=None)

Reset per-sync free/backpressure state and set this sync's barrier target.

live_count is how many consumers take part in THIS sync. Required, no default: a forgotten argument silently targeting 1 would free groups after the FIRST signal while others still pull — use-after-free, not an error. The driver awaits the previous sync's finish before the next begins, so nothing is in flight; a straggler signal would otherwise credit the wrong sync, which is why the consumer drains its signals before finishing.

live_consumer_ids is that same live set enumerated, which is what sizes each slot-sharing group's rendezvous; a count cannot, since a group is a specific set of ids. None leaves every group a singleton.

The packed-destination cache deliberately survives: the layout repeats every sync. So does _plan_digests, which is init-time state.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def begin_sync(
    self, live_count: int, live_consumer_ids: list | None = None
) -> None:
    """Reset per-sync free/backpressure state and set this sync's barrier
    target.

    ``live_count`` is how many consumers take part in THIS sync. Required, no
    default: a forgotten argument silently targeting 1 would free groups after
    the FIRST signal while others still pull — use-after-free, not an error.
    The driver awaits the previous sync's finish before the next begins, so
    nothing is in flight; a straggler signal would otherwise credit the wrong
    sync, which is why the consumer drains its signals before finishing.

    ``live_consumer_ids`` is that same live set enumerated, which is what
    sizes each slot-sharing group's rendezvous; a count cannot, since a group
    is a specific set of ids. ``None`` leaves every group a singleton.

    The packed-destination cache deliberately survives: the layout repeats
    every sync. So does ``_plan_digests``, which is init-time state.
    """
    with self._cache_cond:
        self._gather_error = None
        self._live_count = max(1, int(live_count))
        self._live_ids = (
            set(int(c) for c in live_consumer_ids)
            if live_consumer_ids is not None
            else None
        )
        # Sharing is on only when the geometry shows several deployments;
        # one deployment takes the singleton path.
        self._sharing_active = (
            self._share_width > 0
            and self._live_ids is not None
            and len(self._live_ids) > self._share_width
        )
        self._sharers.clear()
        self._gens.clear()
        self._inflight_groups.clear()
        self._freed_pending.clear()
        self._free_counts.clear()
        self._group_names.clear()
        self._base_cache.clear()
        self._note_progress_locked()

end_sync()

Block until every published group has been freed by its consumers; return the remaining freed keys so the engine drops its last refs.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def end_sync(self) -> list[int]:
    """Block until every published group has been freed by its consumers;
    return the remaining freed keys so the engine drops its last refs."""
    with self._cache_cond:
        self._wait_for(
            lambda: bool(self._inflight_groups),
            "every published group to be freed",
        )
        freed = self._freed_pending
        self._freed_pending = []
        self._base_cache.clear()
        return freed

free_group(group_idx)

Consumer back-edge: one consumer is done with group group_idx, either because its last chunk landed or because it had nothing to pull.

The per-group barrier counts one signal per live consumer against the begin_sync count; every consumer signals every owner, so the target is the same integer everywhere. The last signal drops the cache entries and queues the group as gather credit for wait_freed. A signal arriving before its publish is only counted — publish_group completes it.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def free_group(self, group_idx: int) -> None:
    """Consumer back-edge: one consumer is done with group ``group_idx``,
    either because its last chunk landed or because it had nothing to pull.

    The per-group barrier counts one signal per live consumer against the
    ``begin_sync`` count; every consumer signals every owner, so the target is
    the same integer everywhere. The last signal drops the cache entries and
    queues the group as gather credit for ``wait_freed``. A signal arriving
    before its publish is only counted — ``publish_group`` completes it.
    """
    gi = int(group_idx)
    with self._cache_cond:
        count = self._free_counts.get(gi, 0) + 1
        self._free_counts[gi] = count
        if gi in self._group_names and count >= self._live_count:
            self._release_group_locked(gi)
        self._note_progress_locked()
        self._cache_cond.notify_all()

publish_group(group_idx, entries)

Rebuild one gather group's CUDA-IPC tensors into the serve cache.

NEVER blocks: a gathered group is serveable immediately. The memory bound lives in the engine's gather loop, which stops GATHERING (not publishing) past gather_lookahead unfreed groups.

entries is (storages, views): one CUDA-IPC export per storage, plus per-name (sid, dtype_name, shape, stride, storage_offset) rebuilt here as as_strided views.

Signals can arrive BEFORE their publish — a consumer pulling nothing of a group signals it as its plan starts — so a group whose barrier is already satisfied is released here. Freed groups reach the engine only through wait_freed / end_sync: a freed notice riding an unharvested async publish result would wedge the loop.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def publish_group(self, group_idx: int, entries: tuple) -> None:
    """Rebuild one gather group's CUDA-IPC tensors into the serve cache.

    NEVER blocks: a gathered group is serveable immediately. The memory bound
    lives in the engine's gather loop, which stops GATHERING (not publishing)
    past ``gather_lookahead`` unfreed groups.

    ``entries`` is ``(storages, views)``: one CUDA-IPC export per storage,
    plus per-name ``(sid, dtype_name, shape, stride, storage_offset)`` rebuilt
    here as ``as_strided`` views.

    Signals can arrive BEFORE their publish — a consumer pulling nothing of a
    group signals it as its plan starts — so a group whose barrier is already
    satisfied is released here. Freed groups reach the engine only through
    ``wait_freed`` / ``end_sync``: a freed notice riding an unharvested async
    publish result would wedge the loop.
    """
    storages, views = entries
    bases: dict[int, torch.Tensor] = {}
    for sid, reduce_args in storages.items():
        cached = self._base_cache.get(reduce_args)
        if cached is not None:
            bases[sid] = cached
            continue
        list_args = list(reduce_args)
        # Index 6 of reduce_tensor's args is the exporter's device index;
        # rebuild on this server's device (same physical GPU as the rank).
        list_args[6] = self._device_index
        bases[sid] = self._base_cache[reduce_args] = rebuild_cuda_tensor(*list_args)
    rebuilt: dict[str, torch.Tensor] = {}
    for name, (sid, dtype_name, shape, stride, storage_offset) in views.items():
        typed = bases[sid].view(getattr(torch, dtype_name))
        rebuilt[name] = torch.as_strided(typed, shape, stride, storage_offset)
    del bases

    with self._cache_cond:
        self._cache.update(rebuilt)
        self._inflight_groups.append(group_idx)
        self._group_names[group_idx] = list(rebuilt)
        if self._free_counts.get(group_idx, 0) >= self._live_count:
            self._release_group_locked(group_idx)
        self._note_progress_locked()
        self._cache_cond.notify_all()

rdt_produce_weights_batched(specs, consumer_id=0, seq=-1)

Serve one batched slice request over NIXL.

Waits until the specs' names are cached, then rendezvouses with the other live sharers of this consumer's slot-sharing group. The last to arrive replays each spec's op chain (pure views into cached tensors, guarded by ALLOWED_OPS) and byte-packs the slices 16B-aligned into the group's ring slot seq % nring, mirroring the consumer's identical layout; every sharer then returns that one packed blob, so R deployments cost one pack and one slot instead of R. With a single deployment every group is a singleton, so the arriving call is always its own packer and this is the unshared serve path exactly.

seq is the caller's index in its pull stream to this producer, which is what fixes the slot. Callers that want a single slice pass one spec and read the blob back with that slice's dtype/shape.

Raises:

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
@ray.method(tensor_transport="nixl")
def rdt_produce_weights_batched(
    self, specs: list, consumer_id: int = 0, seq: int = -1
):
    """Serve one batched slice request over NIXL.

    Waits until the specs' names are cached, then rendezvouses with the
    other live sharers of this consumer's slot-sharing group. The last to
    arrive replays each spec's op chain (pure views into cached tensors,
    guarded by ALLOWED_OPS) and byte-packs the slices 16B-aligned into the
    group's ring slot ``seq % nring``, mirroring the consumer's identical
    layout; every sharer then returns that one packed blob, so R deployments
    cost one pack and one slot instead of R. With a single deployment every
    group is a singleton, so the arriving call is always its own packer and
    this is the unshared serve path exactly.

    ``seq`` is the caller's index in its pull stream to this producer, which
    is what fixes the slot. Callers that want a single slice pass one spec
    and read the blob back with that slice's dtype/shape.

    Raises:
        ValueError: ``seq`` was not supplied.
    """
    needed = sorted({n for n, _ in specs})
    if self._served_names is not None:
        unserved = [n for n in needed if n not in self._served_names]
        if unserved:
            # Without this the cache wait below would block forever: this
            # producer never gathers these names, so they can never arrive.
            raise RuntimeError(
                f"pull routed to the wrong producer: {unserved[:3]} "
                f"({len(unserved)} names) are not served here"
            )
    with self._cache_cond:
        self._wait_for(
            lambda: not all(n in self._cache for n in needed),
            f"{len(needed)} name(s) to be published",
        )
        if not all(n in self._cache for n in needed):
            # _wait_for only gives up on a gather error (its own stall
            # included), so the names are never coming.
            raise RuntimeError(
                f"gather errored before {needed}: {self._gather_error!r}"
            )

    if seq < 0:
        # The slot is derived from it, so a caller that does not send one
        # cannot be served safely.
        raise ValueError(
            "rdt_produce_weights_batched requires the consumer's issue index `seq`"
        )
    gen, is_packer = self._join_share_group(consumer_id, seq)
    if not is_packer:
        blob = self._await_shared_pack(gen)
    else:
        try:
            blob = self._pack_shared(gen, specs)
        except BaseException as e:
            self._fail_shared_pack(gen, e)
            raise
    # A served pull is the third of the four progress signals the stall
    # watchdog reads (publish / produce / free / begin_sync): a long sync whose
    # consumers pull steadily but slowly must never trip it.
    with self._cache_cond:
        self._note_progress_locked()
    return [blob]

reserve_serve_buffer(consumer_id, nbytes, plan_digest=None)

Pre-allocate + NIXL-register this consumer's serve ring before any pull, while the fabric is idle (avoids registration races during the sync-0 RDMA churn under M:N fan-in). Idempotent; grows only if needed.

The ring is keyed by SLOT-SHARING GROUP, so the sharers of one group reserve ONE ring between them. They all call this, same group and same size, so the whole body runs under _serve_lock, or two concurrent first-callers would each allocate a ring and one would be dropped while still registered.

plan_digest is the caller's ordered chunk plan for THIS producer. Sharers must agree on it, and this is where a disagreement is caught rather than at serve time, where it is a rendezvous nobody completes.

Raises:

  • RuntimeError

    two consumers of one sharing group disagree on the chunks they pull from this producer.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def reserve_serve_buffer(
    self, consumer_id: int, nbytes: int, plan_digest: str | None = None
) -> None:
    """Pre-allocate + NIXL-register this consumer's serve ring before any
    pull, while the fabric is idle (avoids registration races during the
    sync-0 RDMA churn under M:N fan-in). Idempotent; grows only if needed.

    The ring is keyed by SLOT-SHARING GROUP, so the sharers of one group
    reserve ONE ring between them. They all call this, same group and same
    size, so the whole body runs under ``_serve_lock``, or two concurrent
    first-callers would each allocate a ring and one would be dropped while
    still registered.

    ``plan_digest`` is the caller's ordered chunk plan for THIS producer.
    Sharers must agree on it, and this is where a disagreement is caught
    rather than at serve time, where it is a rendezvous nobody completes.

    Raises:
        RuntimeError: two consumers of one sharing group disagree on the
            chunks they pull from this producer.
    """
    sg = self._share_group(consumer_id)
    if plan_digest is not None:
        with self._cache_cond:
            seen = self._plan_digests.setdefault(sg, {})
            clash = next(
                ((c, d) for c, d in seen.items() if d != plan_digest), None
            )
            if clash is None:
                seen[consumer_id] = plan_digest
        if clash is not None:
            raise RuntimeError(
                f"consumers {clash[0]} and {consumer_id} are in slot-sharing "
                f"group {sg} but do not pull the same chunks from this "
                f"producer ({clash[1]} != {plan_digest}). Sharing a serve "
                f"slot requires the deployments to be identical; check that "
                f"workers_per_replica matches the inference fleet's "
                f"geometry."
            )
    # This is where the producer's NIXL serve memory is committed: one ring
    # of K slots per SHARING GROUP that pulls from this rank -- with a single
    # deployment, one ring per consumer, since every group is a singleton.
    # Routing only sends a consumer here for names this rank owns, so the
    # count is the fan-in, not the fleet.
    #
    # Every slot is sized by the LARGEST chunk that consumer pulls from us,
    # rounded up to a 256 MB multiple, so the whole ring pays for the worst
    # chunk in the plan while most chunks are far smaller.
    #
    # TODO: that over-allocates. Sizing slots per chunk instead of per plan,
    # or capping chunk bytes so no chunk is an outlier, would shrink it; so
    # would dropping the 256 MB round-up, which is slack once a buffer never
    # regrows. Row-splitting the vocab matrix shrinks the outlier itself.
    # The host-staging TODO above would remove these rings altogether.
    alloc = buffer_alloc_bytes(nbytes, self._buffer_presize)
    with self._serve_lock:
        rings = self._serve_rings.setdefault(sg, [None] * self._nring)
        for i in range(self._nring):
            slot = rings[i]
            if slot is None or slot.numel() < alloc:
                rings[i] = self._new_serve_buffer(alloc)

set_gather_error(message)

Record a trainer-side gather failure so blocked serves / publishes stop waiting and surface it.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def set_gather_error(self, message: str) -> None:
    """Record a trainer-side gather failure so blocked serves / publishes
    stop waiting and surface it."""
    with self._cache_cond:
        self._gather_error = RuntimeError(message)
        self._cache_cond.notify_all()

wait_freed()

Block until at least one published group has been freed; return and clear the freed backlog. The engine's gather-credit gate calls this once its loop is gather_lookahead groups ahead, so this wait is where the trainer paces to the consumers' pull rate.

Raises rather than returning empty when the sync errors or stalls: the engine is blocked here inside its gather loop, and an empty return would spin it straight back in.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def wait_freed(self) -> list[int]:
    """Block until at least one published group has been freed; return and
    clear the freed backlog. The engine's gather-credit gate calls this once
    its loop is ``gather_lookahead`` groups ahead, so this wait is where the
    trainer paces to the consumers' pull rate.

    Raises rather than returning empty when the sync errors or stalls: the
    engine is blocked here inside its gather loop, and an empty return would
    spin it straight back in.
    """
    with self._cache_cond:
        self._wait_for(lambda: not self._freed_pending, "a freed-group credit")
        if not self._freed_pending:
            raise RuntimeError(
                "gather errored while waiting for a freed-group credit: "
                f"{self._gather_error!r}"
            )
        freed = self._freed_pending
        self._freed_pending = []
        return freed

warmup_nixl()

Create this server's NIXL agent now, while the rank's GPU is quiet.

Called at spawn, before the server-name all-gather, so no rank can be spinning in a collective. Creating the agent lazily instead deadlocks on EFA-class fabrics (see "warmup_nixl breaks a startup deadlock" in the doc). The warmup buffer stays registered so the agent's CUDA-HMEM path stays initialized.

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
def warmup_nixl(self) -> None:
    """Create this server's NIXL agent now, while the rank's GPU is quiet.

    Called at spawn, before the server-name all-gather, so no rank can be
    spinning in a collective. Creating the agent lazily instead deadlocks on
    EFA-class fabrics (see "warmup_nixl breaks a startup deadlock" in the
    doc). The warmup buffer stays registered so the agent's CUDA-HMEM path
    stays initialized.
    """
    from ray.experimental import register_nixl_memory

    self._nixl_warmup_buf = torch.zeros(1 << 20, dtype=torch.uint8, device="cuda")
    with self._reg_lock:
        register_nixl_memory(self._nixl_warmup_buf)

_SharedPack dataclass

One generation of one slot-sharing group: the rendezvous state for a single chunk, identified by the consumers' issue index.

Attributes:

  • slot (int) –

    seq % ring_depth. Fixed at creation, so slot reuse follows the

Source code in vllm/distributed/weight_transfer/sharded_rdt_trainer.py
@dataclass
class _SharedPack:
    """One generation of one slot-sharing group: the rendezvous state for a
    single chunk, identified by the consumers' issue index."""

    group: int
    slot: int
    """``seq % ring_depth``. Fixed at creation, so slot reuse follows the
    consumers' issue order rather than this side's execution order."""
    arrived: set[int] = field(default_factory=set)
    blob: Any = None
    packing: bool = False
    done: bool = False
    error: BaseException | None = None

slot instance-attribute

seq % ring_depth. Fixed at creation, so slot reuse follows the consumers' issue order rather than this side's execution order.