Skip to content

Cpu proxy ucx backend#1931

Draft
tomerdav wants to merge 18 commits into
ai-dynamo:mainfrom
tomerdav:cpu-proxy-ucx-backend
Draft

Cpu proxy ucx backend#1931
tomerdav wants to merge 18 commits into
ai-dynamo:mainfrom
tomerdav:cpu-proxy-ucx-backend

Conversation

@tomerdav

Copy link
Copy Markdown

What?

Add a CPU-proxy backend for the NIXL GPU Device API, allowing GPU kernels to initiate transfers (PUT, atomic add) on transports without native device-initiated support by delegating submission to CPU worker threads.

  • GPU Device API restructure: split src/api/gpu/ into a backend-agnostic common layer with ucx/ and proxy/ implementations, selected via the NIXL_GPU_DEVICE_BACKEND_PROXY macro and a new gpu_device_api_backend meson option (auto/ucx/proxy/none).
  • Proxy runtime (src/core/device_proxy/): ProxyRuntime manages per-channel GPU-to-CPU submission rings and a memview registry; ProxyWorker threads drain the rings and dispatch through a DeviceProxyBackendAdapter interface.
  • UCX adapter: nixlUcxEngine implements the backend-provider interface; nixlUcxProxyBackend translates proxy submissions into direct UCX RMA puts and atomic adds.
  • Agent API: new nixlAgentConfig fields (enableDeviceProxy, proxyWorkerCount, proxyChannelCount) and nixlAgent::getProxyDeviceContext() for publishing the device context to kernels; remote connection info is propagated to the proxy runtime after metadata exchange.
  • Tests: unit tests for the runtime and memview registry (stub backend, no GPU required), plus an end-to-end proxy_write_test.cu and proxy-mode coverage in single_write_test.cu.

Why?

The GPU Device API currently requires UCX device-side support (GDAKI-capable transports). A CPU proxy makes device-initiated transfers available on any backend that can serve as a proxy adapter, with UCX as the first provider.

How?

      GPU                      pinned host memory                CPU                    transport
┌────────────────┐   submission ring (64B records)   ┌──────────────────────┐   ┌──────────────────┐
│ CUDA kernel    │ ────────────────────────────────► │ ProxyWorker thread   │──►│ DeviceProxy-     │
│ device API     │      write payload, commit          acquire-poll op_idx,  │  │ BackendAdapter   │──► remote
│ proxy enqueue  │      op_idx last (release)          dispatch via adapter  │  │ (UCX RMA put /   │    peer
│                │                                   │                      │   │  atomic add)     │
│ poll completion│ ◄──────────────────────────────── │ publish completions  │   └──────────────────┘
└────────────────┘        completion slot            └──────────────────────┘
  • Each channel is a fixed-depth ring of 64-byte submission records in mapped pinned host memory.
  • The GPU enqueue path writes the record payload first and commits op_idx last with a GPU-scope release; the CPU worker acquire-polls op_idx as the readiness word, dispatches through the backend adapter, and publishes completions back through a pinned completion slot that the GPU polls.
  • To keep the device fast path cheap, the producer index lives in device memory, the consumer index is cached on device and refreshed from host only when the ring appears full, and the proxy context is published via constant memory with force-inlined device helpers.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown

👋 Hi tomerdav! Thank you for contributing to ai-dynamo/nixl.

Your PR reviewers will review your contribution then trigger the CI to test your changes.

🚀

tomerdav added 17 commits July 13, 2026 18:07
Factor the existing UCX-only GPU device headers into a backend-agnostic
common layer (nixl_device_types, nixl_device_api, nixl_device_wrappers)
and move the UCX implementation detail into ucx/nixl_device_impl.cuh.

Add a proxy backend variant under src/api/gpu/proxy/ that compiles into
the nixl_gpu_proxy_lib static library. The proxy headers depend on
proxy_protocol.h (the shared GPU/CPU ring-buffer protocol), which is
introduced here alongside the GPU layer it serves.

The top-level src/api/gpu/nixl_device.cuh dispatches to the selected
backend implementation via the NIXL_GPU_DEVICE_BACKEND_PROXY macro.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Introduce the CPU-side proxy machinery under src/core/device_proxy/:

- proxy_runtime.h/cpp: ProxyRuntime orchestrates channel state and the
  ProxyMemViewRegistry that maps GPU-visible proxy handles to backend
  memory-view descriptors.
- proxy_worker.h/cpp: ProxyWorker is a CPU thread that drains GPU
  submission rings and dispatches transfers through a backend adapter.
- backend_adapter.h: pure-virtual interface for proxy transport backends.
- backend_provider.h: factory interface for backend engines that can
  supply a proxy adapter.

Both new .cpp files are compiled into nixl_lib (src/core/meson.build).

Unit tests for ProxyMemViewRegistry and ProxyRuntime are added under
test/gtest/unit/. They depend only on internal core headers (no public
nixl.h API) and run against a stub backend.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Make nixlUcxEngine inherit from DeviceProxyBackendProvider and implement
createDeviceProxyBackendAdapter(), which returns a nixlUcxProxyBackend.

nixlUcxProxyBackend (src/plugins/ucx/device_proxy/) implements the
DeviceProxyBackendAdapter interface: it translates PreparedProxyTransferDesc
objects into UCX postXfer calls, using the engine's existing UCX endpoint
and memory-registration infrastructure.

The UCX plugin meson.build is updated to compile ucx_proxy_backend.cpp
into the plugin DSO.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Add three fields to nixlAgentConfig: enableDeviceProxy, proxyWorkerCount,
and proxyChannelCount, all defaulting to disabled/1.

Add nixlAgent::getProxyDeviceContext() to the public API, returning the
ProxyDeviceContextData pointer that GPU kernels need to enqueue transfers.

Wire proxy lifecycle into nixlAgentData:
- createProxyRuntime(): called when a backend that implements
  DeviceProxyBackendProvider is registered and enableDeviceProxy is set.
  Initialises ProxyRuntime and starts worker threads.
- shutdownProxyRuntime(): called from the agent destructor.

nixl_listener.cpp: propagate remote connection info into the proxy runtime
(proxyRuntime->loadRemoteConnInfo) so proxy workers can post to remote
peers after metadata exchange.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Add proxy_write_test.cu: an end-to-end device-API test that stands up a
ProxyRuntime with a stub backend, publishes the device context to the GPU
via nixlProxyPublishContext(), enqueues PUT operations from a CUDA kernel,
and polls for completion.

Extend single_write_test.cu with conditional proxy-backend support: when
compiled with -DNIXL_GPU_DEVICE_BACKEND_PROXY the test uses the proxy code
path (nixlProxyPublishContext / nixlProxyClearContext) instead of the UCX
device API path.

Introduce the gtest_proxy executable in test/gtest/meson.build.
It links nixl_gpu_proxy_lib directly (so Meson includes nixl_device_proxy.cu.o
in the RDC device-link step) and is compiled with -DNIXL_GPU_DEVICE_BACKEND_PROXY.
Gate it on cuda_dep.found() rather than the UCX GPU flag used by gtest.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Change implicit behavior of UCX device api selection to explicit by adding a new option `gpu_device_api_backend`:
auto  -> choose default backend if available
ucx   -> require UCX GPU Device API
proxy -> require CUDA/proxy support
none  -> disable default GPU Device API backend

Also centralize backend-specific compile args, deps, flags and link inputs so tests and examples will use the same resolved GPU Device API selected backend.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Route proxy PUT through a lower-level UCX RMA write helper while preserving the existing request handle and flush-backed completion semantics.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
And make sure it is reflected in proxy memview registry as it only requires remote memview

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Publish proxy submissions by writing the payload first and committing op_idx last, then have the CPU worker acquire-poll op_idx instead of ready_flag.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Narrow protocol-only submission fields and assert the 64-byte record layout while keeping op_idx at the first word for readiness publication.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
…emory

Publish the proxy context through constant memory and force-inline device helper paths used by proxy API calls.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Move the GPU-owned producer index to device memory, derive operation ids from it, and keep mapped host aliases local to allocation.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Cache the host-pinned consumer index in device memory and refresh from host only when the ring appears full.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Keep the ready-word publish at GPU scope for host-pinned proxy records. The enqueue path writes the payload first and commits op_idx last; the CPU worker acquire-polls op_idx before copying the record.

This avoids the enqueue overhead of a system-scope release while documenting the D2H FIFO contract explicitly.

Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
Signed-off-by: Tomer Davidor <tdavidor@nvidia.com>
@tomerdav
tomerdav force-pushed the cpu-proxy-ucx-backend branch from dcb1e30 to 95a5503 Compare July 13, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant