Summary
Adding cuda_copy to the UCX_TLS allowlist causes a significant drop in LLM inference throughput (requests-per-second) in our serving runtime. When cuda_copy is excluded from UCX_TLS, QPS returns to baseline. We'd like the maintainers' input on where in the cuda_copy transport this overhead might originate and how to keep cuda_copy available as a fallback without the throughput penalty.
Environment
- UCX version & Transport info:
$ ucx_info -v
# Library version: 1.19.1
# Library path: /usr/local/lib/libucs.so.0
# API headers version: 1.19.1
# Git branch 'v1.19.x', revision 812388c
# Configured with: --disable-logging --disable-debug --disable-assertions --disable-params-check --enable-mt --prefix=/usr/local --enable-shared --disable-static --disable-doxygen-doc --enable-optimizations --enable-cma --enable-devel-headers --with-cuda=/usr/local/cuda --with-verbs --with-dm
$ ucx_info -d | grep Transport
# Transport: self
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: tcp
# Transport: sysv
# Transport: posix
# Transport: cuda_copy
# Transport: cuda_ipc
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: rc_verbs
# Transport: ud_verbs
# Transport: dc_mlx5
# Transport: rc_mlx5
# Transport: ud_mlx5
# Transport: cma
- Host CUDA driver / runtime: Driver version 580.95.05 & CUDA 13.0
- Docker Container CUDA version: CUDA 12.9
- GPU: H200 * 8
- OS / kernel:
# Host
$ cat /etc/os-release
NAME="Rocky Linux"
VERSION="9.3 (Blue Onyx)"
ID="rocky"
ID_LIKE="rhel centos fedora"
VERSION_ID="9.3"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Rocky Linux 9.3 (Blue Onyx)"
ANSI_COLOR="0;32"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:rocky:rocky:9::baseos"
HOME_URL="https://rockylinux.org/"
BUG_REPORT_URL="https://bugs.rockylinux.org/"
SUPPORT_END="2032-05-31"
ROCKY_SUPPORT_PRODUCT="Rocky-Linux-9"
ROCKY_SUPPORT_PRODUCT_VERSION="9.3"
REDHAT_SUPPORT_PRODUCT="Rocky Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="9.3"
# Container
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.2 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
- Interconnect:
$ nvidia-smi topo -m
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 NIC0 NIC1 NIC2 NIC3 NIC4 NIC5 NIC6 NIC7 CPU Affinity NUMA Affinity GPU NUMA ID
GPU0 X NV18 NV18 NV18 NV18 NV18 NV18 NV18 PIX PIX NODE NODE SYS SYS SYS SYS 0-55,112-167 0 N/A
GPU1 NV18 X NV18 NV18 NV18 NV18 NV18 NV18 PIX PIX NODE NODE SYS SYS SYS SYS 0-55,112-167 0 N/A
GPU2 NV18 NV18 X NV18 NV18 NV18 NV18 NV18 NODE NODE PIX PIX SYS SYS SYS SYS 0-55,112-167 0 N/A
GPU3 NV18 NV18 NV18 X NV18 NV18 NV18 NV18 NODE NODE PIX PIX SYS SYS SYS SYS 0-55,112-167 0 N/A
GPU4 NV18 NV18 NV18 NV18 X NV18 NV18 NV18 SYS SYS SYS SYS PIX PIX NODE NODE 56-111,168-223 1 N/A
GPU5 NV18 NV18 NV18 NV18 NV18 X NV18 NV18 SYS SYS SYS SYS PIX PIX NODE NODE 56-111,168-223 1 N/A
GPU6 NV18 NV18 NV18 NV18 NV18 NV18 X NV18 SYS SYS SYS SYS NODE NODE PIX PIX 56-111,168-223 1 N/A
GPU7 NV18 NV18 NV18 NV18 NV18 NV18 NV18 X SYS SYS SYS SYS NODE NODE PIX PIX 56-111,168-223 1 N/A
NIC0 PIX PIX NODE NODE SYS SYS SYS SYS X PIX NODE NODE SYS SYS SYS SYS
NIC1 PIX PIX NODE NODE SYS SYS SYS SYS PIX X NODE NODE SYS SYS SYS SYS
NIC2 NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE X PIX SYS SYS SYS SYS
NIC3 NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE PIX X SYS SYS SYS SYS
NIC4 SYS SYS SYS SYS PIX PIX NODE NODE SYS SYS SYS SYS X PIX NODE NODE
NIC5 SYS SYS SYS SYS PIX PIX NODE NODE SYS SYS SYS SYS PIX X NODE NODE
NIC6 SYS SYS SYS SYS NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE X PIX
NIC7 SYS SYS SYS SYS NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE PIX X
Legend:
X = Self
SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node
PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
PIX = Connection traversing at most a single PCIe bridge
NV# = Connection traversing a bonded set of # NVLinks
NIC Legend:
NIC0: mlx5_0
NIC1: mlx5_1
NIC2: mlx5_2
NIC3: mlx5_3
NIC4: mlx5_4
NIC5: mlx5_5
NIC6: mlx5_6
NIC7: mlx5_7
- Runtime that consumes UCX: an in-house LLM inference engine that uses NIXL on top of UCX for P2P KV Cache transfer. In our benchmark, only one process runs on one GPU and the code execution path does not touch real RDMA Read/Write: we just init
nixlAgent with the ucx backend and register a DRAM region and a VRAM region, then the QPS of inference decreases noticeably. The real KV Cache transfer path goes through the cudaMemcpyAsync and cudaMemcpy2DAsync APIs without any RDMA operation.
Setup
We run a benchmark that sends a fixed batch of inference requests to the runtime and measures end-to-end throughput. The only variable between the two configurations below is whether cuda_copy is allowed in UCX_TLS.
- Baseline (no
cuda_copy):
export UCX_TLS=ib,tcp,self,sm
# benchmark invocation
- With
cuda_copy:
export UCX_TLS=ib,tcp,self,sm,cuda_copy # or simply default (no restriction)
# same benchmark invocation
All other UCX_* environment variables are identical between the two runs.
Observations
| Run |
UCX_TLS |
QPS (req/s) |
| Baseline |
ib,tcp,self,sm (cuda_copy excluded) |
2952 |
cuda_copy enabled |
ib,tcp,self,sm,cuda_copy (cuda_copy allowed) |
2411 |
In our runs the QPS drop is roughly 18%. The regression is fully reproducible and only depends on whether cuda_copy is in the TLS list.
What we confirmed
-
The regression disappears when cuda_copy is excluded from UCX_TLS. Toggling UCX_TLS alone reproduces or fixes the issue; no other change is needed.
-
The penalty is paid even when cuda_copy carries no traffic. In our runtime, cuda_copy is listed because we register a VRAM region for RDMA communication. However in our benchmark execution path, the real KV Cache transfer path is within one process by calling cudaMemcpyAsync and cudaMemcpy2DAsync API.
- For KV Cache Offload: We firstly call
cudaMemcpy2DAsync to gather the per-layer KV Cache from Paged Attention VRAM region to the VRAM middle buffer region (which is registered for RDMA communication), then we call cudaMemcpyAsync to transfer the KV Cache to lower DRAM tier (the whole DRAM tier region is registered for RDMA communication).
- For KV Cache Onload: Similar to Offload but just reverse, we first call
cudaMemcpyAsync to transfer the KV Cache from DRAM to VRAM middle buffer, then call cudaMemcpy2DAsync to scatter the all-layers KV Cache to the per-layer Paged Attention VRAM region.
Questions for the maintainers
-
What global side effects does cuda_copy MD open (or component load) have on the CUDA context or driver state that could affect the performance of unrelated CUDA work in the same process? We see the regression even when cuda_copy is never selected for a transfer.
-
Is there a supported way to keep cuda_copy in UCX_TLS (as a fallback) without the throughput penalty, or is excluding it via UCX_TLS=^cuda_copy the intended workaround? We'd prefer to keep the fallback available.
-
Are there other transports (e.g. cuda_ipc) that apply similar process-global side effects on MD open that we should be aware of?
-
Is there any guidance on which transports are safe to keep in UCX_TLS for a latency-sensitive, high-concurrency CUDA application that relies on async copy/kernel overlap?
Reproducer sketch
Since the benchmark is based on our in-house inference engine, it's hard to provide a full reproducer script, but the minimal reproduction steps are:
- Build UCX with CUDA support.
- Construct
nixlAgent with the ucx backend in-process.
- Start a CUDA application that relies on async
cudaMemcpyAsync overlap for throughput (in our case, an LLM inference server with one thread dedicated to inference and another to request preprocessing, which drives data offloading and onloading).
- Run the workload twice, toggling only
UCX_TLS:
UCX_TLS=ib,tcp,self,sm (cuda_copy excluded) — baseline QPS.
UCX_TLS left default, or cuda_copy explicitly allowed — QPS drops by ~18% (from 2952 to 2411 req/s).
Happy to provide logs, ucx_info output, or any other information required.
Summary
Adding
cuda_copyto theUCX_TLSallowlist causes a significant drop in LLM inference throughput (requests-per-second) in our serving runtime. Whencuda_copyis excluded fromUCX_TLS, QPS returns to baseline. We'd like the maintainers' input on where in thecuda_copytransport this overhead might originate and how to keepcuda_copyavailable as a fallback without the throughput penalty.Environment
$ nvidia-smi topo -m GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 NIC0 NIC1 NIC2 NIC3 NIC4 NIC5 NIC6 NIC7 CPU Affinity NUMA Affinity GPU NUMA ID GPU0 X NV18 NV18 NV18 NV18 NV18 NV18 NV18 PIX PIX NODE NODE SYS SYS SYS SYS 0-55,112-167 0 N/A GPU1 NV18 X NV18 NV18 NV18 NV18 NV18 NV18 PIX PIX NODE NODE SYS SYS SYS SYS 0-55,112-167 0 N/A GPU2 NV18 NV18 X NV18 NV18 NV18 NV18 NV18 NODE NODE PIX PIX SYS SYS SYS SYS 0-55,112-167 0 N/A GPU3 NV18 NV18 NV18 X NV18 NV18 NV18 NV18 NODE NODE PIX PIX SYS SYS SYS SYS 0-55,112-167 0 N/A GPU4 NV18 NV18 NV18 NV18 X NV18 NV18 NV18 SYS SYS SYS SYS PIX PIX NODE NODE 56-111,168-223 1 N/A GPU5 NV18 NV18 NV18 NV18 NV18 X NV18 NV18 SYS SYS SYS SYS PIX PIX NODE NODE 56-111,168-223 1 N/A GPU6 NV18 NV18 NV18 NV18 NV18 NV18 X NV18 SYS SYS SYS SYS NODE NODE PIX PIX 56-111,168-223 1 N/A GPU7 NV18 NV18 NV18 NV18 NV18 NV18 NV18 X SYS SYS SYS SYS NODE NODE PIX PIX 56-111,168-223 1 N/A NIC0 PIX PIX NODE NODE SYS SYS SYS SYS X PIX NODE NODE SYS SYS SYS SYS NIC1 PIX PIX NODE NODE SYS SYS SYS SYS PIX X NODE NODE SYS SYS SYS SYS NIC2 NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE X PIX SYS SYS SYS SYS NIC3 NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE PIX X SYS SYS SYS SYS NIC4 SYS SYS SYS SYS PIX PIX NODE NODE SYS SYS SYS SYS X PIX NODE NODE NIC5 SYS SYS SYS SYS PIX PIX NODE NODE SYS SYS SYS SYS PIX X NODE NODE NIC6 SYS SYS SYS SYS NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE X PIX NIC7 SYS SYS SYS SYS NODE NODE PIX PIX SYS SYS SYS SYS NODE NODE PIX X Legend: X = Self SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI) NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU) PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge) PIX = Connection traversing at most a single PCIe bridge NV# = Connection traversing a bonded set of # NVLinks NIC Legend: NIC0: mlx5_0 NIC1: mlx5_1 NIC2: mlx5_2 NIC3: mlx5_3 NIC4: mlx5_4 NIC5: mlx5_5 NIC6: mlx5_6 NIC7: mlx5_7nixlAgentwith theucxbackend and register a DRAM region and a VRAM region, then the QPS of inference decreases noticeably. The real KV Cache transfer path goes through thecudaMemcpyAsyncandcudaMemcpy2DAsyncAPIs without any RDMA operation.Setup
We run a benchmark that sends a fixed batch of inference requests to the runtime and measures end-to-end throughput. The only variable between the two configurations below is whether
cuda_copyis allowed inUCX_TLS.cuda_copy):cuda_copy:All other
UCX_*environment variables are identical between the two runs.Observations
UCX_TLSib,tcp,self,sm(cuda_copyexcluded)cuda_copyenabledib,tcp,self,sm,cuda_copy(cuda_copyallowed)What we confirmed
The regression disappears when
cuda_copyis excluded fromUCX_TLS. TogglingUCX_TLSalone reproduces or fixes the issue; no other change is needed.The penalty is paid even when
cuda_copycarries no traffic. In our runtime,cuda_copyis listed because we register a VRAM region for RDMA communication. However in our benchmark execution path, the real KV Cache transfer path is within one process by callingcudaMemcpyAsyncandcudaMemcpy2DAsyncAPI.cudaMemcpy2DAsyncto gather the per-layer KV Cache from Paged Attention VRAM region to the VRAM middle buffer region (which is registered for RDMA communication), then we callcudaMemcpyAsyncto transfer the KV Cache to lower DRAM tier (the whole DRAM tier region is registered for RDMA communication).cudaMemcpyAsyncto transfer the KV Cache from DRAM to VRAM middle buffer, then callcudaMemcpy2DAsyncto scatter the all-layers KV Cache to the per-layer Paged Attention VRAM region.Questions for the maintainers
What global side effects does
cuda_copyMD open (or component load) have on the CUDA context or driver state that could affect the performance of unrelated CUDA work in the same process? We see the regression even whencuda_copyis never selected for a transfer.Is there a supported way to keep
cuda_copyinUCX_TLS(as a fallback) without the throughput penalty, or is excluding it viaUCX_TLS=^cuda_copythe intended workaround? We'd prefer to keep the fallback available.Are there other transports (e.g.
cuda_ipc) that apply similar process-global side effects on MD open that we should be aware of?Is there any guidance on which transports are safe to keep in
UCX_TLSfor a latency-sensitive, high-concurrency CUDA application that relies on async copy/kernel overlap?Reproducer sketch
Since the benchmark is based on our in-house inference engine, it's hard to provide a full reproducer script, but the minimal reproduction steps are:
nixlAgentwith theucxbackend in-process.cudaMemcpyAsyncoverlap for throughput (in our case, an LLM inference server with one thread dedicated to inference and another to request preprocessing, which drives data offloading and onloading).UCX_TLS:UCX_TLS=ib,tcp,self,sm(cuda_copyexcluded) — baseline QPS.UCX_TLSleft default, orcuda_copyexplicitly allowed — QPS drops by ~18% (from 2952 to 2411 req/s).Happy to provide logs,
ucx_infooutput, or any other information required.