MPComm Transport#
Overview#
UCL-MPComm (Unified Communication Library - Memory Pool Communication) is a high-performance RDMA
communication library for heterogeneous memory pooling, developed by the Tencent Astral Network
Team. In Mooncake it is integrated as a transport backend of the TENT transfer framework and
registered as MPCOMM; the library is shortened to MPComm throughout this document.
Upstream repository: Tencent/UCL-MPComm
MPComm drives multiple RDMA NICs concurrently and performs two-level load balancing (across NICs
and across QPs within a NIC), with NUMA-aware worker placement. It exposes one-sided
put/get primitives that the transport maps onto TENT’s WRITE/READ requests.
At runtime, TransferEngineImpl loads MPComm Transport when USE_MPCOMM is enabled at build time
and transports/mpcomm/enable=true is set in the configuration.
TENT only. This transport is implemented for the TENT runtime (
mooncake-transfer-engine/tent/) and is not available through the legacy Transfer Engine transport path. Builds must therefore enableUSE_TENTtogether withUSE_MPCOMM, and benchmarks must run with--backend=tent.
Code Structure#
The transport is split so that everything reaching MPComm goes through one interface. That keeps the
provider out of most translation units, which is what makes the TENT-side logic buildable and
testable without libmpcomm - the same arrangement TpuPjrtShim and UrmaAdapter use for their
providers.
Unit |
Responsibility |
Needs |
|---|---|---|
|
The MPComm boundary. A thin pass-through: MPComm owns its slicing, NIC/QP selection and worker threads, so there is no scheduling to model here. Compiles to an unavailable stub when |
yes (only here) |
|
Peer cache and endpoint attribute parsing: which peers are connected, whose keys are current, and who may talk to a peer at any moment |
no |
|
Request and completion mapping: WRITE/READ onto put/get, provider outcome onto |
no |
|
What needs the TENT runtime: resolving a |
no |
MpcommTransport also accepts an adapter through a second constructor, which is how tests
substitute the provider.
Transfer Pipeline#
Init:
MPComm::init()is called with the local segment name as the MPComm host id, the RDMA device list derived from the TENTTopology, and a TCP port used for MPComm’s own metadata handshake.startAcceptThread()then serves incoming handshakes.Advertisement: the transport publishes
v1:<host>:<mpcomm_tcp_port>into the local segment’stransport_attrsunder theMPCOMMkey, viaSegmentManager::updateLocal()followed bysynchronizeLocal(). Peers read this to learn where to complete the MPComm handshake. Thev1:prefix makes a later format change detectable instead of silently misparsed; an attribute without a prefix is read asv1. Only IPv4 endpoints are accepted.Registration:
addMemoryBuffer()callsregisterMemory()andpublishBuffer()so the buffer’s rkeys become visible to peers. NUMA placement is auto-detected by MPComm.Connection: on the first request to a peer,
ensurePeerConnected()resolves the peer’s MPComm endpoint from itstransport_attrs, callsconnect(), and then fetches the peer’s memory keys withqueryRemoteBuffer(). Concurrent callers for the same peer wait for the first one rather than starting a second handshake.The cache is keyed by MPComm host id (the segment name), not by
SegmentID, because that is what MPComm keys connections by. Closing and reopening a segment yields a freshSegmentIDfor the same peer, and a secondconnect()to an already connected peer replaces its connection record wholesale - discarding the keys it carries and leaking its queue pairs - so keying on theSegmentIDwould reconnect a peer that is already connected.For the same reason the connection and the keys are tracked separately. A connection cannot be closed, so once established it is kept and reused; if only the key query failed, the peer is left in
CONNECTED_NO_KEYSand the next request retries the query alone. A connection that was never established is not cached at all, so the next request retries the full handshake.Keys are also refetched when the peer registers memory after they were fetched, which TENT permits at any time. The transport compares the peer’s currently published buffer ranges against those the cached keys cover; a range that is not covered triggers a refresh. Unregistered ranges do not, since a key for memory the peer no longer publishes is never used. Because segment descriptors are cached per thread with a TTL, a newly registered buffer becomes visible only after that TTL expires.
Execution:
submitTransferTasks()issuesputAsync()forWRITEandgetAsync()forREAD, one MPComm transfer per request. MPComm performs its own slicing and NIC/QP selection internally.Completion:
getTransferStatus()polls lazily withisTransferComplete(), then readsgetTransferResult()to obtain the error code and a byte count, and releases the handle. The count is of bytes posted, which equals the request length once MPComm reports success; a transfer reported as successful but short is demoted to failed rather than trusted.
Additional Dependencies#
In addition to Mooncake’s base dependencies, MPComm Transport requires the MPComm library:
Header:
${MPCOMM_ROOT}/include/mpcomm.hLibrary:
${MPCOMM_ROOT}/lib/libmpcomm.soVersion: 1.4 or newer, and major version 1. Configuration reads MPComm’s CMake package config and fails with an explicit message if the version is older or the major differs - MPComm declares
SameMajorVersioncompatibility, so a different major is an ABI break by its own definition. Both the standalone install and the wheel ship that config, and the resolved version is printed asMPComm version: <x.y.z>. A prefix that has only the headers and the library copied into it reportsunknowninstead, and the version is then not checked.
Both are provided by an MPComm installation, which may come either from a standalone CMake install
or from the MPComm Python wheel (in which case MPCOMM_ROOT is the mpcomm package directory
inside site-packages, since the wheel ships include/ and lib/ under the package root).
The two routes are not necessarily equivalent: MPComm gates several features behind build options
that default to OFF, notably USE_CUDA (device memory support) and USE_MLNX (Mellanox-specific
QP tuning that spreads traffic across ECMP paths, which affects multi-QP throughput). Its own
build.sh turns them on, whereas a plain pip install does not. Check how the library you install
was configured if device memory or multi-QP performance matters.
Make sure libmpcomm.so can be found by the dynamic linker at run time, for example via
LD_LIBRARY_PATH.
Wheel Packaging#
The mooncake-transfer-engine wheel deliberately does not bundle MPComm. scripts/build_wheel.sh
passes --exclude libmpcomm.so* to auditwheel repair, so engine.so keeps its DT_NEEDED entry
on libmpcomm.so.<N> and the library stays an external dependency resolved at run time. This keeps
MPComm independently upgradable: replacing libmpcomm.so does not require rebuilding or repackaging
Mooncake, as long as the MPComm major version (its SOVERSION) is unchanged. A major bump does
require rebuilding Mooncake against the new headers.
Consequently, importing the Python extension without MPComm available fails with:
ImportError: libmpcomm.so.1: cannot open shared object file: No such file or directory
Provide the library through either MPComm installation form, then point the linker at it:
# From the MPComm wheel
export MPCOMM_ROOT=$(python3 -c "import mpcomm, os; print(os.path.dirname(mpcomm.__file__))")
# ...or from a standalone CMake install, e.g. MPCOMM_ROOT=/opt/mpcomm
export LD_LIBRARY_PATH=$MPCOMM_ROOT/lib:$LD_LIBRARY_PATH
python3 -c "from mooncake import engine" # should now import cleanly
Build and Compile#
Prerequisites
MPComm is installed, providing both
include/mpcomm.handlib/libmpcomm.soRDMA devices are available and
libibverbsis installedBuild environment can access Mooncake and its base dependencies
CMake Configuration
# Clone Mooncake
git clone https://github.com/kvcache-ai/Mooncake.git
cd Mooncake
# Enable TENT + MPComm. Add -DUSE_CUDA=ON if you need VRAM segments.
mkdir build && cd build
cmake .. -DUSE_TENT=ON -DUSE_MPCOMM=ON -DMPCOMM_ROOT=/opt/mpcomm
# Build
make -j$(nproc)
MPCOMM_ROOT is mandatory when USE_MPCOMM=ON. Configuration fails early if it is unset, or if
the expected header and library cannot be found underneath it. On success the configure log
reports the resolved paths:
-- MPComm transport is enabled
-- MPComm include: /opt/mpcomm/include
-- MPComm library: /opt/mpcomm/lib/libmpcomm.so
Enabling the Transport#
Loading the transport and selecting it for a transfer are two separate steps.
1. Load it. The transport is only instantiated when its config gate is on:
{ "transports": { "mpcomm": { "enable": true } } }
2. Select it. Any of the standard TENT mechanisms work:
A transport policy (see Transport Selector):
{ "transports": { "mpcomm": { "enable": true } }, "policy": [ { "name": "mpcomm_memory", "segment_type": "memory", "transports": ["mpcomm"] } ] }
A per-request override, which takes precedence over policies:
Request r{}; r.transport_hint = TransportType::MPCOMM;
A policy or a hint is effectively required. In the default ordering returned by
getSupportedTransports(), MPCOMM comes second to last – only TPU follows it – so leaving
the choice to the default means MPComm is unlikely to be picked at all.
Run and Test#
tebench (the TENT benchmark, built at build/mooncake-transfer-engine/benchmark/tebench)
supports mpcomm for connectivity and performance validation. The role is determined by
--target_seg_name: empty means target, otherwise initiator.
There is also a unit test, which is built when USE_MPCOMM=ON and unit tests are enabled. Its
functional case needs RDMA devices and MPComm at run time and skips itself otherwise:
ctest -R tent_mpcomm_transport_test --output-on-failure
--xport_type=mpcomm selects the MemoryOptions.type used when registering buffers. It also
restricts the enabled transports, but setting MC_TENT_CONF replaces the configuration wholesale
and undoes that (see Important Notes), so the examples below list the
transports gates explicitly. Keeping them in a shell variable avoids repeating the block:
XPORTS='"transports":{"mpcomm":{"enable":true},"rdma":{"enable":false},
"tcp":{"enable":false},"shm":{"enable":false},"nvlink":{"enable":false},
"mnnvl":{"enable":false},"gds":{"enable":false},"io_uring":{"enable":false}}'
# Terminal 1: target
MC_TENT_CONF="{\"rpc_server_hostname\":\"10.0.0.1\",$XPORTS}" \
MPCOMM_TCP_PORT=13579 \
./tebench --backend=tent --xport_type=mpcomm \
--metadata_type=p2p --rpc_server_port=12345 \
--seg_type=DRAM --total_buffer_size=2147483648
# Terminal 2: initiator
MC_TENT_CONF="{\"rpc_server_hostname\":\"10.0.0.2\",$XPORTS}" \
MPCOMM_TCP_PORT=13579 \
./tebench --backend=tent --xport_type=mpcomm --tent_transport_hint=mpcomm \
--metadata_type=p2p --rpc_server_port=12346 \
--target_seg_name=10.0.0.1:12345 \
--seg_type=DRAM --total_buffer_size=2147483648 \
--op_type=read --start_block_size=262144 --max_block_size=262144 \
--start_batch_size=32 --max_batch_size=32 \
--start_num_threads=4 --max_num_threads=4 --duration=30
Setting rpc_server_hostname is strongly recommended on multi-homed or containerized hosts; see
Important Notes. --seg_name is deliberately absent: with metadata_type=p2p
TENT derives the local segment name from rpc_server_hostname and rpc_server_port and ignores
the flag.
On a single host both processes share the port namespace, so give them different handshake ports. Avoid the 15000-17000 range, which the TENT RPC server allocates from:
MPCOMM_TCP_PORT=13579 ... # target
MPCOMM_TCP_PORT=13580 ... # initiator
To confirm that traffic really went over MPComm, look for these lines:
MpcommTransport: Installed successfully, host_id=10.0.0.2:12346, tcp_port=13579, devices=mlx5_0,mlx5_1
MpcommTransport: Connected to segment 10.0.0.1:12345
If MPComm fails to start, the engine only logs Transport mpcomm skipped: ... and continues with
the remaining transports. With every other transport disabled the run then fails later and less
obviously, when buffer registration finds no usable transport.
Add --check_consistency=true to verify payload correctness. It writes and reads every block back,
so it changes the access pattern as well as lowering the reported bandwidth; leave it off when
measuring throughput.
Unit Tests#
There are two suites, because the data path needs hardware and the logic around it does not.
tent_mpcomm_boundary_test drives the transport against an injected MpcommAdapter, so it needs
neither an RDMA device nor libmpcomm and is built in every configuration:
cmake .. -DUSE_TENT=ON -DBUILD_UNIT_TESTS=ON # USE_MPCOMM not required
make -j tent_mpcomm_boundary_test
ctest -R tent_mpcomm_boundary_test --output-on-failure
It covers endpoint publication and parsing, single-flight connection under concurrency, key-query retry over an existing connection, refreshing keys when a peer registers memory, the WRITE/READ mapping, short-transfer and error handling, releasing each handle exactly once, and teardown. It does not cover MPComm’s own behaviour - slicing, NIC and QP selection, worker scheduling - which is the provider’s responsibility.
tent_mpcomm_transport_test exercises the real data path: it forks a target, drives a WRITE followed
by a READ over MPComm and verifies the payload. It requires RDMA devices and a working MPComm
installation, and skips itself when the engine cannot be brought up:
cmake .. -DUSE_TENT=ON -DUSE_MPCOMM=ON -DBUILD_UNIT_TESTS=ON -DMPCOMM_ROOT=<prefix>
make -j tent_mpcomm_transport_test
ctest -R tent_mpcomm_transport_test --output-on-failure
Parent and child use distinct MPCOMM_TCP_PORT values derived from the pid, since MPComm’s handshake
listener would otherwise collide; see the note on port uniqueness above.
GPU (Device) Memory#
MPComm transfers device memory as well as host DRAM, so VRAM segments are supported in all four combinations (DRAM to DRAM, DRAM to GPU, GPU to DRAM, GPU to GPU).
Three preconditions must all hold, and only the first one is checked at run time:
nvidia-peermemis loaded. Device memory is registered through the ordinaryibv_reg_mrpath: the kernel’sget_user_pages()is intercepted bynvidia-peermem(ib_peer_memory_client) to pin GPU pages. There is no dma-buf fallback. This is the same dependencyRdmaTransporthas for GPU-Direct.MPComm itself was built with
-DUSE_CUDA=ON. That option defaults toOFF, and its device detection is compiled out entirely when it is off, so device pointers are then registered as host memory and NUMA/PCIe affinity selection silently degrades. The upstreambuild.shenables it; a plainpip installof the MPComm package does not.Mooncake was built with
-DUSE_CUDA=ON, otherwise--seg_type=VRAMis rejected outright.
The transport probes /proc/modules during install() and advertises the GPU capabilities only
when nvidia-peermem is present. Otherwise it logs
MpcommTransport: nvidia_peermem not detected, GPU memory support is disabled
and reports dram_to_dram only, so transport selection will not route device memory to MPComm.
Setting transports/mpcomm/disable_gpu_direct_rdma to true forces the same behaviour on a host
that does have the module, which is reported separately:
MpcommTransport: GPU memory support disabled by transports/mpcomm/disable_gpu_direct_rdma
Note that conditions 2 and 3 are not detectable by that probe. If GPU transfers behave oddly while the module is loaded, confirm how MPComm was built.
Buffer registration is attempted for every loaded transport irrespective of capabilities, so on a
host without the module registering a VRAM buffer logs a warning from MPComm. That is harmless:
the buffer simply does not list MPCOMM among its transports, and selection skips it.
To exercise device memory with tebench, pass --seg_type=VRAM on either or both sides, or
--seg_type_mix=dram,vram to drive both memory types from a single process:
./tebench --backend=tent --xport_type=mpcomm --seg_type=VRAM ...
Environment Variables#
MPComm reads its own tuning parameters directly from the environment. The transport adds the two
*_TCP_PORT variables.
Variable |
Description |
Default |
|---|---|---|
|
Local TCP port for MPComm’s metadata handshake. Must be unique per process on a host: MPComm binds it and fails to initialise if it is taken, with no retry. |
|
|
Peer handshake port, used only when the peer published no |
(none; peer is rejected) |
|
RoCE/IB GID index used when creating QPs. Devices whose GID at this index is all zeroes are skipped, which can leave MPComm reporting no usable devices. Use |
|
|
Comma-separated list of allowed RDMA device names. When unset, the transport passes the device list derived from the TENT |
(TENT topology) |
|
Maximum bytes per RDMA operation; larger requests are sliced across NICs |
|
|
Number of QPs per NIC per connection. Values above 16 make initialisation fail |
|
|
Maximum work completions per |
|
|
Number of idle polling iterations a worker spins (with a CPU pause hint) before backing off |
|
|
QP send queue depth |
|
|
Maximum outstanding work requests per QP |
|
|
|
|
|
Print statistics every N transfers (requires |
|
Configuration Options (TENT)#
transports/mpcomm/enable: enable or disable the transport (default:trueonceUSE_MPCOMMis compiled in)transports/mpcomm/disable_gpu_direct_rdma: force the GPU capabilities off even whennvidia-peermemis loaded (default:false). See GPU (Device) Memory.
MPComm’s own behaviour is tuned through the environment variables above rather than through TENT configuration keys.
Important Notes#
TENT only. There is no MPComm backend on the legacy Transfer Engine transport path, so
--backend=classicandtransfer_engine_bench --protocol=mpcommare not supported.Set
rpc_server_hostnameon multi-homed or containerized hosts. Withmetadata_type=p2p, TENT overwriteslocal_segment_namewithrpc_server_hostname:port. Whenrpc_server_hostnameis unset it is auto-discovered, which may pick an address that peers cannot route to (for example a container overlay address). Since the publishedtransport_attrsare derived from that name, MPComm would then advertise an unreachable endpoint.tebenchhas no command-line flag for it, so pass it throughMC_TENT_CONF.MC_TENT_CONFreplaces the configuration wholesale. Keys other than the metadata identity (metadata_type,metadata_servers,local_segment_name,rpc_server_hostname,rpc_server_port) are not preserved across the load. If you setMC_TENT_CONFand rely on--xport_typeto disable the other transports, list thetransportsgates explicitly inMC_TENT_CONFas well, otherwise they revert to their defaults.Segment metadata mutation. The transport publishes its endpoint through
SegmentManager::updateLocal(). Snapshots returned bygetLocal()are copy-on-write and must never be written through.Buffer registration cost.
registerMemory()pins and maps memory on every NIC, so registration of large buffers takes noticeable time at startup. Register once and reuse.Optional transport interfaces are not implemented. MPComm Transport does not provide cancellation (
supportsCancellation()returnsfalse), notification (supportNotification()returnsfalse, sotebench --notifiis unavailable), bandwidth estimation, or NIC load statistics, and it does not consume theqp_poolor progress-notification facilities ofTransport::SubBatch. Failover and QoS features that depend on those hooks fall back to their defaults.The first transfer to a peer performs the MPComm handshake inline. Connection setup happens on the submitting thread the first time a segment is used, so that submission takes noticeably longer than subsequent ones.
Security and Trust Boundary#
MPComm’s metadata handshake is a plain TCP exchange with no authentication and no encryption, and what it exchanges is remote memory metadata:
struct RemoteBufferEntry {
uint64_t addr;
uint64_t length;
int numa_node;
std::vector<uint32_t> rkeys; // <- remote keys
};
An address together with its rkey is exactly what an RDMA READ or WRITE needs. Anything that can
reach MPCOMM_TCP_PORT can therefore obtain the means to read and write the process’s registered
memory directly, without going through TENT at all.
Treat that port as being at the same trust level as the RDMA fabric itself. In practice:
Keep it on the same trusted network as the fabric, and do not expose it to untrusted networks or to the internet. Restrict it with host firewall rules if the host also carries untrusted traffic.
Registered buffers are reachable by any peer that completes the handshake. There is no per-peer authorisation, and no per-buffer permission model beyond what the fabric enforces.
This is the same exposure model as raw RDMA between trusted nodes; MPComm adds no protection of its own and does not weaken the fabric’s either.
Addressing#
Only IPv4 endpoints are supported. The handshake sockets are
AF_INET, and the endpoint attribute is parsed as<ipv4>:<port>; an IPv6 literal is rejected at parse time rather than being silently split at the wrong colon.The listener binds all interfaces (
INADDR_ANY) and this is not configurable - it is MPComm’s own behaviour. Use firewall rules to restrict which interfaces are actually reachable.The advertised address follows
rpc_server_hostname, since the endpoint attribute is derived from the local segment name. On a multi-homed host this ties the handshake path to whichever address TENT uses for RPC. Settingrpc_server_hostnameexplicitly is therefore the way to control it today; a dedicated setting for advertising an address separate from the RPC one is not implemented yet.
Troubleshooting#
Configuration fails with a missing MPCOMM_ROOT#
USE_MPCOMM=ON requires MPCOMM_ROOT to point at the MPComm install prefix
Pass -DMPCOMM_ROOT=<prefix>. The prefix must contain include/mpcomm.h and
lib/libmpcomm.so (lib64 is searched as well).
Configuration fails with MPComm not found#
MPComm not found under MPCOMM_ROOT=<prefix>
Verify the layout, and remember that CMake caches find results:
ls $MPCOMM_ROOT/include/mpcomm.h $MPCOMM_ROOT/lib/libmpcomm.so
cmake .. -UMPCOMM_LIBRARY -UMPCOMM_INCLUDE_DIR
The peer connects to an unexpected address#
Symptom: the initiator logs MPComm: Connecting to <host_id> at <ip>:<port> with an address peers
cannot reach, and stalls, while the segment itself was opened successfully.
The address MPComm uses comes from the peer’s segment name and transport_attrs, which are
derived from rpc_server_hostname. Set it explicitly on both sides, then confirm the target
advertises the intended address:
MpcommTransport: Installed successfully, host_id=<expected-ip>:<port>, ...
MPComm: No RDMA devices found#
MPComm skips any device whose GID at MPCOMM_GID_INDEX (default 3) is all zeroes, which is
common on InfiniBand and on RoCE setups with a different GID layout. Set MPCOMM_GID_INDEX=-1 to
pick the first non-zero GID, or point it at the correct index.
Failed to query remote buffers#
MpcommTransport: Failed to query remote buffers from <host>, error=<code>
The connection succeeded but fetching the peer’s memory keys did not. Those keys are the only way to address the peer’s memory, so this is a hard failure and the request fails.
The connection itself is kept - MPComm cannot close one, and reconnecting would replace its
connection record and leak its queue pairs - so the peer is left in CONNECTED_NO_KEYS and the
next request retries only the query, not the handshake.
The usual cause is that the peer had not finished registering its buffers yet, so start the target and let it finish registration before starting the initiator. Since the retry is a query, the recovery needs no restart on either side.
Lower than expected bandwidth#
Remove
--check_consistency=true; it writes and reads every block back.Increase
--duration; the inline handshake on the first transfer to a peer is included in the measurement.Increase
--start_batch_size/--max_batch_size; with a batch size of 1 there are not enough in-flight requests to fillMPCOMM_MAX_OUTSTANDING_PER_QP.Check
MPCOMM_MAX_RDMA_TRANSFER_SIZEagainst the block size. Slicing is what spreads a transfer over several NICs, so a request smaller than this limit becomes a single chunk on a single NIC. Lower it to engage more NICs per request.For
--seg_type=DRAM,tebenchallocates one buffer of--total_buffer_sizeper NUMA node. Restricting memory to a single node withnumactl --membindwhile NICs on another node drive traffic results in cross-socket access.