One-sided tensor transports (experimental)#
Created On: Oct 16, 2026 | Last Updated On: Oct 16, 2026
torch.distributed._transport moves registered tensor byte ranges between
independently managed workers. It does not require a process group, global rank
assignment, or matching receives.
An endpoint connects to one peer. Exchange bind() bytes and remote memory
descriptors through a trusted application control plane. Exchange descriptors only with authorized peers. A write copies from a local view to the remote base; a read
copies from the remote base to a writable local view. Offsets and lengths are bytes;
shape and dtype agreement is the application’s responsibility.
Synchronous reads/writes return zero; async_op=True returns
torch.distributed.Work. Successful completion means the transfer completed,
not that the remote application consumed or acknowledged the data. Asyncio callers
can use read_async, write_async, or wait_all. Registration remains valid
until unregistration or close, and tensors must not be resized or have their storage replaced.
Ordinary reads and writes are not ordered on CUDA streams. The API does not
provide tracing, batching, or remote slicing. Descriptor classes define explicit
serialize()/deserialize() methods.
The built-in backends declare their fields in a versioned JSON envelope; binary
metadata is base64-encoded. Unknown fields, versions, backends, and invalid field
types are rejected. Tensor contents and native handles are never serialized.
- torch.distributed._transport.new_transport(backend, device=None, **kwargs)[source]#
Construct a one-sided transport, optionally restricting tensor devices.
- Parameters:
backend (str) – Name registered with
register_transportor an installedtorch.distributed.transportsentry point.device (torch.device | str | None) – Optional CPU/CUDA device restriction; otherwise infer each tensor’s device at registration.
**kwargs (Any) – Options forwarded to the selected backend’s constructor. Backend modules are imported only when selected.
- Return type:
Example:
import torch from torch.distributed._transport import new_transport backend = "my_backend" # An installed or process-registered backend. # Both endpoints are shown locally. Across processes, exchange bind() # results and remote descriptors through your application's control plane. with ( new_transport(backend, "cpu") as trainer, new_transport(backend, "cpu") as replica, ): trainer_url, replica_url = trainer.bind(), replica.bind() trainer.connect(replica_url) replica.connect(trainer_url) weights = torch.arange(8, dtype=torch.float32) received = torch.empty_like(weights) source = trainer.register_memory(weights) target = replica.register_memory(received) descriptor = target.to_remote_buffer() # Exchange these bytes through the application control plane. remote = type(descriptor).deserialize(descriptor.serialize()) work = trainer.write(source.to_view(), remote, async_op=True) work.wait() torch.testing.assert_close(received, weights) # Coordinate with peers before closing either endpoint. # Inside an asyncio application, after registration and metadata exchange: async def push_weights(trainer, source, remote): await trainer.write_async(source.to_view(), remote, timeout=30.0) # Coordinate with peers before cleanup; cancellation does not cancel DMA. await trainer.close_async(timeout=30.0)
- torch.distributed._transport.new_transport_rank(backend, device=None, *, store=None, peer_rank, rank=None, bootstrap_timeout=30.0, **kwargs)[source]#
Construct, bind, and connect a transport through a Store.
- Parameters:
backend (str) – Registered transport backend name.
device (torch.device | str | None) – Optional tensor device restriction, as in
new_transport.store (dist.Store | None) – Shared rendezvous Store, such as
torch.distributed.TCPStore; defaults to the default process group’s Store. No collectives are performed.peer_rank (int) – Nonnegative integer identifying the peer in this Store namespace.
rank (int | None) – Local identity; defaults to
torch.distributed.get_rank(). An explicit override requires no initialized process group. Identities need not be contiguous, but must be distinct for the two peers.bootstrap_timeout (float | timedelta) – Timeout for each of bind, the Store waits, connect, and cleanup, in seconds or as a
timedelta. Does not change the Store’s timeout.**kwargs (Any) – Forwarded to
new_transportand the backend constructor.
- Return type:
Example:
import torch.distributed as dist from torch.distributed._transport import new_transport_rank dist.init_process_group("gloo") # Pair ranks 0 and 1; other ranks need not participate. transport = new_transport_rank( "nixl", "cpu", peer_rank=1 - dist.get_rank(), ) # Exchange registered-memory descriptors separately. Coordinate with # peers before unregistering memory or closing the transport.
Calls are matched in order per rank pair and backend: the Nth call on each peer connects to the other’s Nth call, including failed calls. One Store can therefore connect a rank to every other rank. Bootstrap failures attempt to close the partially created transport.
- class torch.distributed._transport.Transport(device=None)[source]#
Base class for one-sided tensor transports.
readandwritereturn zero on synchronous success. Withasync_op=True, they return atorch.distributed.Workwhosewaitblocks until completion and propagates transfer errors.is_completedincludes failed operations. Work futures resolve to an empty list on success.read_asyncandwrite_asyncare asyncio coroutines. Cancellation or timeout may leave transfers pending.wait_allawaits their completion futures without blocking the event loop.Operations use byte ranges, not tensor shapes or dtypes. One endpoint has one outgoing peer; no process group, ranks, or matching receives are needed. Exchange connection bytes and remote descriptors through a trusted control plane. Registration is valid until unregistration or close, including after bind/connect.
timeoutis a nonnegative, finite number of seconds;Noneselects the backend default. A timeout bounds the caller’s wait, not the transfer’s lifetime: it does not cancel DMA. Pending work retains its local buffers. After a timeout, wait for the Work or successfully close the transport before reusing buffers. A timed-out close rejects new operations but retains resources until close is retried successfully. Independent transfers may overlap; wait before submitting dependent or overlapping reads/writes. There is no implicit completion ordering.Never resize, replace storage, or modify buffers while registered/exposed to a peer. The application must coordinate remote access and notify peers before unregistering memory or closing; local completion does not establish that a peer has stopped accessing this endpoint. Descriptors are invalid after unregistration or their owner closes.
read_streamandwrite_streamorder transfers on the current CUDA stream: the transfer starts after prior work on the stream, and later work waits for it to complete. They return after enqueueing, without holding SMs while the transfer is in flight, and can be captured in CUDA graphs. Transfer failures terminate the process, since consumers may already be enqueued. Ordinaryreadandwritedo not interact with CUDA streams.- abstract bind(*, timeout=None)[source]#
Bind the endpoint and return its opaque connection URL.
- Return type:
- abstract close(*, timeout=None)[source]#
Drain outstanding operations and release transport resources.
- async close_async(*, timeout=None)[source]#
Await cleanup when supported by the backend.
Unlike
close, this must not block the event loop while waiting for transfers. Blocking prototype backends do not implement this method.
- abstract connect(peer_url, *, timeout=None)[source]#
Connect to a bound peer and return zero on success.
Rank-to-endpoint lookup belongs in a separate application control-plane adapter; this interface addresses peers using opaque connection bytes.
- Return type:
- abstract read(local_buffer, remote_buffer, *, async_op=False, timeout=None)[source]#
Read into a local view; return Work for async_op=True, otherwise zero.
- async read_async(local_buffer, remote_buffer, *, timeout=None)[source]#
Read into a local view, yielding to asyncio until the transfer finishes.
- read_stream(local_buffer, remote_buffer, *, stream=None, timeout=None)[source]#
Read into a local view in order on
stream(default: current).
- abstract register_memory(tensor, *, timeout=None)[source]#
Register a contiguous tensor, including after bind/connect or transfers.
Exchange its remote-buffer descriptor with the peer before remote access.
- Return type:
- abstract static supported()[source]#
Return whether the transport can be used in this process.
- Return type:
- unregister_memory(memory, *, timeout=None)[source]#
Unregister memory after coordinating with peers to stop remote access.
Wait for locally submitted transfers using this registration first. Reused registrations share one lifetime: unregistering any handle invalidates all aliases, existing views, and exported descriptors. Register the tensor again and exchange fresh descriptors before reuse. Repeating unregistration on the same handle is a no-op while open. Backends without this operation raise
NotImplementedError.
- abstract write(local_buffer, remote_buffer, *, async_op=False, timeout=None)[source]#
Write a local view; return Work for async_op=True, otherwise zero.
- class torch.distributed._transport.Memory(*args, **kwargs)[source]#
Memory registered with a transport.
- class torch.distributed._transport.MemoryView(*args, **kwargs)[source]#
A read-only view of registered memory.
- class torch.distributed._transport.MutableMemoryView(*args, **kwargs)[source]#
A view explicitly permitting writes to its registered memory.
- class torch.distributed._transport.RemoteBuffer(*args, **kwargs)[source]#
A backend-defined wire descriptor, excluding tensor contents/native handles.
Exchange
serialize()bytes and reconstruct them with the matching backend descriptor class’sdeserialize(). Decoders validate their schema and version without executing code or importing classes named by the payload. Descriptors grant memory access: exchange them only with authorized peers.
- async torch.distributed._transport.wait_all(works, *, timeout=None)[source]#
Await Work futures without blocking the asyncio loop.
Timeout and cancellation stop waiting, not transfers. Retain buffers and wait again (or close the transport) before reusing them. Transfer errors and iterable errors are reported after draining submitted work, unless this wait is timed out or cancelled first. Each Work must support
get_future. This adapter does not select CUDA streams or establish consumer-stream ordering; callers must follow their backend’s CUDA synchronization contract.
- torch.distributed._transport.available_transports()[source]#
Return registered and discoverable transport names.
- torch.distributed._transport.register_transport(name, factory, *, replace=False)[source]#
Register a transport factory for this process.
CUDA streams#
read_stream and write_stream order a transfer on stream, or the
current CUDA stream if None. The transfer starts after prior work on the
stream; later work waits for it to complete. Calls return after enqueueing and
no kernel runs while the transfer is in flight.
with torch.cuda.stream(stream):
source.copy_(producer)
transport.write_stream(source_view, remote_destination)
transport.read_stream(destination_view, remote_source)
consume(destination)
Stream transfers can be captured with torch.cuda.graph; each replay submits
a new transfer. Register buffers and warm up transfers before capture. Captured
buffers stay registered until close; replaying after close terminates
the process.
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
source.copy_(producer)
transport.write_stream(source_view, remote_destination)
graph.replay()
The default implementation awaits write_async/read_async on a shared
asyncio loop thread and gates the stream with a CUDA stream memory wait. It requires Linux
and GPUDirect RDMA write ordering, and allows 64 outstanding transfers per
stream; captured transfers hold their slot until close. Since consumers may
already be enqueued, a failed transfer terminates the process. Backends with
native stream support may override these methods.
NIXL backend#
The initial backend is NIXL, installed separately with pip install nixl.
Its default UCX plugin supports CPU and CUDA memory, subject to the installed
NIXL/UCX build and hardware.
unregister_memory(memory) deregisters a local allocation without closing the
transport. Wait for local transfers first and coordinate with peers to stop remote
access; the backend cannot detect incoming DMA. All handles sharing a registration,
including previously created views and exported descriptors, become invalid.
Register again and exchange fresh descriptors before resuming transfers.
NIXL transfers return Work objects that retain their request handles until
completion. wait_all, read_async, and write_async await Work futures.
The NIXL adapter resolves those futures by checking native transfer status.
Each live transfer owns a distinct request handle.
Independent requests may overlap; explicitly wait before issuing dependent or
overlapping reads/writes. Completion ordering is not implicit.
NIXL’s default wait timeout is 30 seconds. timeout is in seconds; None
selects the backend default and zero polls without waiting. Work.wait instead
takes a datetime.timedelta; its zero default selects the transfer’s timeout.
Timeout and asyncio cancellation stop waiting, not DMA. The transport retains
pending requests and buffers, even if the caller drops its Work. Wait again or
successfully close before reusing buffers. Coordinate with peers before closing
exposed memory; close only drains locally submitted operations.
NIXLTransport.close_async awaits pending transfers before native cleanup.
A timed-out or cancelled close rejects new work and retains resources; retry
close to finish cleanup. Registrations keep the transport alive even after its last outgoing transfer,
because peers may still access exposed memory. Forgotten registrations may retain
resources indefinitely:
call unregister_memory or close after coordinating with peers.
Checking is_completed also releases completed requests.
For pending NIXL work, get_future requires a running asyncio loop; the loop
must remain running to drive that future. Completed work needs no event loop.
Native metadata, registration, request submission, status checks, and cleanup calls execute synchronously on the calling thread. Their timeouts bound lock acquisition and transfer completion waits, not execution inside NIXL. Python cannot interrupt a blocked native call, even when it releases the GIL.
- class torch.distributed._transport.nixl.NIXLTransport(device=None, *, plugin='UCX', agent_name=None, num_threads=0, enable_prog_thread=True, capture_telemetry=False, backend_options=None, timeout=30.0)[source]#
One-sided tensor transport backed by a NIXL plugin.
Backend options accepted by
new_transport()includeplugin="UCX",agent_name=None,num_threads=0,enable_prog_thread=True,capture_telemetry=False,backend_options=None, andtimeout=30.0.backend_optionsmaps plugin parameter names to strings and is forwarded to NIXL’screate_backend. NIXL is an optional dependency, imported only when selected. Transfers require registered views; raw tensors are rejected rather than allocated or registered implicitly.- async close_async(*, timeout=None)[source]#
Await outstanding DMA before synchronous native resource cleanup.
Cancellation/timeout retains resources and rejects new submissions. Retry close after pending work completes. Native cleanup is not interruptible; peer access must already have been stopped externally.