mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #2662] [RFC]: Add TPU Transport Support via PJRT #698
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#698
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Liwink on GitHub (Jun 29, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/2662
Originally assigned to: @staryxchen on GitHub.
Changes proposed
Changes proposed
1. Summary
We propose adding TPU as a first-class accelerator target in Mooncake's Transfer Engine (TENT). The implementation introduces a
TpuTransportclass that enables KVCache disaggregation between TPU-based Prefill and Decode nodes, using a staged host-DRAM pipeline over existing TENT transports (RDMA / TCP) — without requiring any proprietary TPU SDK at build time.The key architectural insight is that TPU KVCache transfer decomposes into two independent halves that map cleanly onto TENT's existing capabilities:
BufferToHostBuffer/CopyToHostAsync)TpuTransportThis decomposition avoids flooding the TPU's Inter-Chip Interconnect (ICI) with asynchronous KVCache traffic, preserving ICI bandwidth for synchronous model-parallel collectives.
Estimated scope: ~800–1200 LOC (excluding tests).
2. Motivation
2.1 The Problem
Google Cloud TPUs (v4, v5e, v5p, v6e) are a major LLM serving platform — powering production workloads on JAX/MaxText, PyTorch/XLA, and vLLM-TPU. However, Mooncake currently has zero TPU support, despite supporting 8+ other accelerator platforms (NVIDIA CUDA, AMD ROCm, Huawei Ascend, Cambricon MLU, Moore Threads MUSA, MetaX MACA, Hygon DCU, Iluvatar CoreX).
This gap prevents the entire Google Cloud TPU ecosystem from benefiting from Mooncake's KVCache disaggregation, hierarchical storage, and P2P distribution capabilities.
2.2 Why Not GPUDirect RDMA?
Unlike NVIDIA GPUs, TPUs do not expose third-party NIC access to device HBM:
cuMemAlloc+ibv_reg_mrPjRtBufferlibtpu.so(closed-source runtime)This means Mooncake's existing transport path (register device buffer → NIC DMA read/write) cannot work directly for TPU. A staged pipeline through host DRAM is required.
2.3 Why TENT?
TPU KVCache disaggregation needs the same infrastructure that TENT already provides:
Topology::discover()✅ControlService::bootstrap()✅Transport::registerLocalMemory()✅submitTransfer/getTransferStatus✅Building this outside TENT would duplicate significant effort.
3. Technical Design
3.1 Architecture Overview
3.2 Data Flow
The full KVCache transfer pipeline follows a staged 2-copy model:
Why bypass the TPU ICI network?
Google's Inter-Chip Interconnect (ICI) is a high-bandwidth 2D/3D torus hardwired between TPUs within a Pod slice, optimized for synchronous model-parallel collectives (
AllReduce,AllGather). Flooding ICI with asynchronous All-to-All KVCache traffic between disaggregated Prefill/Decode nodes causes contention that degrades matrix-multiply latency.By staging KVCache through host DRAM and transmitting over multi-NIC datacenter networks (gVNIC or host RDMA), the TPU ICI links remain fully available for compute.
Division of labor
BufferToHostBufferTpuTransport::stageOut()Transport::submitTransfer()BufferFromHostBufferTpuTransport::stageIn()3.3 Layer 1: C++
TpuTransport3.3.1 Zero Build-Time Dependency via
dlopenZero Build-Time Dependency: To ensure standard Linux build farms continue compiling smoothly,
TpuTransportuses POSIXdlopen/dlsymat runtime to bind symbols fromlibtpu.soorlibpjrt_c_api.so. This feature is gated behindoption(USE_TPU "Enable TPU PJRT runtime support" OFF). Nofind_packageor proprietary SDK is needed.3.3.2 Build System Integration
The build guard follows the established pattern from
USE_CUDA,USE_ROCM,USE_ASCEND, etc. inCMakeLists.txtandmooncake-common/common.cmake.3.3.3 Staging Buffer Pool
3.4 Layer 2: Python
TPUAllocatorMirroring the existing
NVLinkAllocatorandBarexAllocatorinmooncake-integration/allocator.py:3.5 Layer 3: Zero-Copy Shared Memory
When the serving engine (e.g., vLLM worker) and the Mooncake networking daemon run as separate processes on the same TPU VM host:
shm_open/mmapShmDescriptor(name + offset + length) to the TENT daemon via an IPC socketmemcpycost and transmits over the networkThis avoids the memory bus saturation that would occur with inter-process
memcpyof 500MB+ KVCache chunks.3.6 Memory Location & Topology Integration
3.7
patchelfBuild Consideration4. Files Changed (Estimated Touchpoints)
Following the established contribution pattern from prior hardware PRs (#2118, #2019):
CMakeLists.txt,mooncake-common/common.cmakeUSE_TPUoption, conditional compilation guardsmooncake-transfer-engine/include/transport/transport.hTpuTransportin transport enum/factorymooncake-transfer-engine/src/transport/tpu_transport/tpu_transport.{h,cpp}TpuTransportimplementationmooncake-transfer-engine/src/transport/tpu_transport/pjrt_loader.{h,cpp}dlopen/dlsymPJRT C API loadermooncake-transfer-engine/src/transport/tpu_transport/staging_buffer_pool.{h,cpp}mooncake-transfer-engine/src/memory_location.cppmooncake-transfer-engine/src/topology.cppmooncake-store/include/gpu_staging_utils.hmooncake-store/include/pinned_buffer_pool.hmooncake-integration/allocator.pyTPUAllocatorclassmooncake-transfer-engine/tests/test_tpu_transport.cppmooncake-integration/tests/test_tpu_allocator.pydocs/source/getting_started/supported-protocols.md5. Implementation Plan
We propose splitting this into 3 incremental PRs to facilitate review:
PR 1:
[TransferEngine] Add TpuTransport with PJRT dynamic loading(~500 LOC)USE_TPUoption in CMakeListspjrt_loader.{h,cpp}: Runtimedlopen/dlsymresolutiontpu_transport.{h,cpp}: Core Transport interface implementationstaging_buffer_pool.{h,cpp}: POSIX shared memory allocatormemory_location.cpp/topology.cpp: TPU address + topologyPR 2:
[Integration] Add TPUAllocator for vLLM/SGLang(~200 LOC)TPUAllocatorclass inallocator.pypatchelfbuild hook for symbol collision avoidancePR 3:
[Doc] Add TPU transport documentation and examples(~100 LOC)supported-protocols.mdupdates6. Testing Strategy
TpuTransportlogic, staging pool alloc/free,dlopenerror pathsCI compatibility: Since
USE_TPU=OFFby default and all TPU dependencies aredlopen'd at runtime, enablingUSE_TPU=ONin CI requires no TPU hardware — only the mock tests run. Real hardware tests are contributor-gated.7. FAQ / Anticipated Questions
Q: Why not use ICI for KVCache transfer?
ICI is a fixed-topology, synchronous-optimized torus network designed for
AllReduce/AllGathercollectives within a Pod slice. Injecting asynchronous, unpredictable A2A KVCache traffic causes contention that degrades matrix-multiply throughput. Staging through host DRAM + datacenter NIC keeps ICI 100% available for compute.Q: What's the performance overhead of the staged pipeline?
The staged pipeline adds ~2 PCIe DMA copies (HBM→DRAM, DRAM→HBM) relative to a hypothetical GPUDirect path. On TPU v5e with PCIe Gen5 x16:
The pipeline is double-buffered so staging and network transfer overlap, hiding most of the latency.
Q: Does this add any build-time dependencies?
No. The PJRT C API is loaded at runtime via
dlopen("libtpu.so")ordlopen("libpjrt_c_api.so"). WhenUSE_TPU=ON, the build only requires standard POSIX headers (dlfcn.h,sys/mman.h). The resulting binary runs on any Linux system and gracefully fails with a descriptive error if no PJRT runtime is found.Before submitting a new issue...
Before submitting a new issue...
@github-actions[bot] commented on GitHub (Jun 29, 2026):
Thanks for opening this issue, @Liwink!
6659863A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@staryxchen commented on GitHub (Jun 30, 2026):
Thanks for the detailed RFC! The motivation and staged-pipeline approach make sense for TPU's hardware constraints. Before diving into implementation details, I'd like to discuss two architectural questions:
1. Multi-transport collaboration: who owns the orchestration?
The proposed
TpuTransportencapsulates a 3-stage pipeline:In the current architecture, each Transport subclass handles one hop —
RdmaTransportdoes NIC-to-NIC,HipTransportdoes VRAM-to-VRAM via fabric memory, etc. They are composed as peers insideMultiTransport, not nested.The middle hop (host DRAM → remote host DRAM) already has dedicated transports (RDMA/TCP). If
TpuTransportinternally invokes another transport instance, it breaks the flat composition model.An alternative decomposition:
TpuTransportonly handles HBM ↔ Host DRAM (the PJRT DMA stage)This keeps each transport single-responsibility. But it raises questions about who orchestrates the full pipeline and how staging buffers are managed across the boundary.
Question: How do you envision
TpuTransportinteracting with RDMA/TCP for the middle hop? Should it call into another transport directly, or should there be a higher-level orchestrator (e.g., at theTransferEngine/ TENT level) that chains the stages?2. Target: Legacy
TransferEngineImplor TENT?The RFC's code examples target the legacy interface (
Transport::submitTransfer,registerLocalMemory). However, TENT (mooncake-transfer-engine/tent/) is actively developed with its own transport hierarchy (tent::Transport,tent::ShmTransport, etc.) and already has shared-memory infrastructure that overlaps with the proposedStagingBufferPool.Given the direction of the project, should this land on TENT from the start? That would:
ShmTransportfor the staging buffer / IPC layerControlService/TopologyAPIs directlyOr is there a reason to target the legacy path first (e.g., vLLM-TPU integration currently depends on legacy TE)?
Looking forward to your thoughts on these two points — they'll shape the PR structure significantly.
@staryxchen commented on GitHub (Jun 30, 2026):
Thoughts on simplifying the implementation path
Thanks again for this RFC — the problem analysis and staged-pipeline design are solid, and TPU support is something we'd love to see in Mooncake.
After digging into the TENT internals, I wanted to share an observation that might actually make your life easier: TENT already has a staging mechanism (
ProxyManager) that implements the same 3-stage pattern you described. This could significantly reduce the amount of code you need to write.How ProxyManager works today
ProxyManager(tent/src/runtime/proxy_manager.cpp) handles the case where device memory can't be directly accessed by the NIC — exactly the TPU situation:It already provides chunked double-buffering, async status tracking (
TaskInfo::staging/staging_status), and stage buffer lifecycle management. The pipeline overlap optimization you described in §3.2 is essentially built-in.A potentially lighter approach
Rather than implementing a full Transport subclass (with peer discovery, memory export, batch state machine, routing capabilities, etc.), you might be able to achieve TPU support by plugging PJRT into ProxyManager's existing staging path:
PjrtLoader—dlopen+ symbol resolution (you already designed this well in §3.3.1)MTYPE_TPU— extend theMemoryTypeenum so the routing layer knows when staging is neededcudaMemcpyfor GPU↔DRAM, add a PJRT-based path for TPU↔DRAMThis could bring the scope down to ~200–300 LOC in a single PR, while still delivering full end-to-end TPU KVCache transfer. The
USE_TPUbuild gating and dlopen strategy from your RFC would carry over as-is.Why this might be a better fit
The existing Transport implementations (NVLink, MNNVL, SHM, RDMA) all share a common trait: they are complete peer-to-peer data channels with handle exchange, remote memory mapping, and direct transfer capability. PJRT's
BufferToHostBuffer/BufferFromHostBufferis more of a local device I/O operation (similar in nature tocudaMemcpy D2H/H2D) — it doesn't have a remote peer or handle exchange. ProxyManager was designed precisely for wrapping this kind of local staging operation and chaining it with a real transport.Suggested exploration
If this direction sounds reasonable, I'd suggest starting with:
ProxyManager::transferEventLoop()to see how staging is orchestrated todayibv_reg_mr(for RDMA registration)Of course, if you've identified reasons why ProxyManager doesn't fit the TPU case (e.g., specific lifetime or threading constraints from the PJRT runtime), happy to discuss alternatives. Just wanted to flag this existing infrastructure since it seems like a shortcut.
@Liwink commented on GitHub (Jun 30, 2026):
Hi Stary, Thanks a lot for detailed suggestion. Plugging PJRT into
ProxyManagermakes a lot sense!I'm reading through ProxyManager::transferEventLoop() now to start prototyping the PJRT staging path. Thanks again for pointing out this shortcut which makes the architecture so much cleaner!
@Liwink commented on GitHub (Jul 3, 2026):
Hi @staryxchen — following up on your ProxyManager suggestion. I've prototyped it and have first PR ready to submit; posting a summary here first to confirm alignment before opening it.
TL;DR
The ProxyManager direction works well and the staging pipeline is reused as-is — no new pipeline code, and no standalone networked
TpuTransportlike my original RFC. It lands entirely on TENT (not legacy TE), gated behind-DUSE_TPU(OFF by default), so CI and existing builds are unaffected.One architectural finding worth sharing: the scope ended up a bit larger than "plug PJRT into the copy step," for a reason that only shows up when tracing the routing layer.
Finding: the copy seam is clean, but routing needs a capability provider
ProxyManagerdoesn't callcudaMemcpydirectly. The local device↔host stage is submitted as a normalLOCAL_SEGMENT_IDsub-transfer (ProxyManager::submitLocalStage→submitStagingTransfer→submitTransfer), which is then routed like any other transfer — and routing only selects a transport that advertises the matching capability (gpu_to_dram/dram_to_gpu) viacheckAvailability/TransportSelector::isTransportAvailable. The bytes actually move inside the selected transport'ssubmitTransferTasks, which callsPlatform::copy.For CUDA, the transport answering "yes, I can do
gpu_to_dram" isNVLinkTransport. On a TPU host, nothing answers it:ShmTransportonly advertisesdram_to_dram, andRdmaTransportonly sets GPU caps whennvidia_peermemis present. So a PJRT-backedPlatform::copywould exist but never be reached.The fix is a thin local-stage executor transport — it advertises only
gpu_to_dram/dram_to_gpu(nevergpu_to_gpu, so cross-node TPU traffic is always staged through host DRAM) and itssubmitTransferTasksjust runsPlatform::copyforLOCAL_SEGMENT_IDrequests. No network path, no peer discovery, no handle exchange. Most of its line count is theTransportinterface surface, not logic.PR #1 contents (~1.1k LOC incl. tests + docs; core is ~500)
MTYPE_TPUmemory type +"tpu:N"location parsing, wired into both routing modes' device-type checks (isGpuTypeintransfer_engine_impl.cppand the selector'sis_gpu— they're separate and both need it).TpuPlatform(derivesCpuPlatform): host DRAM + NIC topology inherited; only device-aware paths overridden (copy,getMemoryType,getLocation, per-deviceMemEntryinprobe).TpuTransportas above.findStagingPolicyTPU case: local HBM↔host via the TPU transport, host↔host via whichever host transport is present. Note: typical cloud TPU deployments are TCP/multi-NIC, not RDMA, so the case is gated onRDMA || TCPand the cross stage is routed bydram_to_dramcapability rather than hardcoded — TCP is selected when RDMA is absent, RDMA used if available.tpu_pjrt_abi.h) resolved viadlopen(TpuPjrtShim). TENT keeps zero build-time PJRT/XLA dependency, matching the RFC's original intent. Adapter path defaults tolibmooncake_tpu_pjrt.so, overridable viaMC_TPU_PJRT_LIB; if absent, the build still links and runs (TPU ops return non-OK Status).supported-protocols.md, plus aREADME.md(adapter contract) andSTAGING_ARCHITECTURE.md(the routing/capability walkthrough above, with code pointers) next to the platform code.This also answers the
ibv_reg_mrquestion you raised: staging buffers are allocated and RDMA-registered by TENT itself (ProxyManager::allocateStageBuffers→impl_->registerLocalMemory), and the adapter copies into that already-registered buffer — so there's no PJRT-allocated host memory on the RDMA path in PR #1.Confirming three things before I open the PR
ShmTransport/the capability layer to handle device↔host generically? I chose the separate transport to avoid changing SHM semantics for all platforms.If this looks right I'll open the PR — happy to break it into smaller pieces if you'd prefer.
@staryxchen commented on GitHub (Jul 3, 2026):
Re: architecture and PR scope
Thanks for the thorough writeup — the analysis and code pointers are precise, and the capability-provider observation is a genuinely nice architectural insight.
Platform::copyalone would never be reached without a transport advertising the capability; glad you caught this before writing the code.On your three questions
1. Shape — thin dedicated transport vs. teaching ShmTransport / capability layer generically. Dedicated thin transport is the right call. Teaching ShmTransport to advertise
gpu_to_dram/dram_to_gpuwould change what SHM means to every existing deployment (a user configuring TCP+SHM would suddenly see SHM claim GPU staging). A generic "device-local staging transport" refactor would be premature — worth revisiting only when a second non-CUDA accelerator actually needs the same shape. Building the concrete TPU case first and generalizing later is the right order.2. Target — TENT, not legacy TE. Confirmed, aligned with my earlier point about avoiding legacy tech debt.
3. Scope split — first PR = C++/TENT data path, follow-ups for JAX / DMA-pinned buffers / benchmarks. Well-scoped. The JAX-first choice neatly sidesteps the PyTorch/XLA symbol-collision problem the original RFC flagged (the
patchelfworkaround) — postponing that until it's actually needed keeps this PR focused. Splitting DMA-mapped pinned staging buffers into a follow-up also makes sense: the interaction between DMA-map and RDMA registration on the same buffer is a distinct optimization worth landing on its own with proper benchmarks.Two small findings that may be worth double-checking
Both strengthen your approach rather than change it:
a)
TransportSelector::isTransportAvailablehas a hardcoded same-machine check attransport_selector.cpp:322:Your thin TPU transport is also local-stage-only. Worth extending this check to include the TPU transport type so it can never be selected for a non-
LOCAL_SEGMENT_IDrequest by mistake.b)
findStagingPolicycurrently gates staging cases onNVLINKpresence (e.g.,if (transport_list_[RDMA] && transport_list_[NVLINK])at line 1294 and similar for MNNVL at line 1316). Your TPU case will presumably usetransport_list_[TPU]as the analogous gate — just flagging so the pattern is consistent with the existing cases.Both are the kind of thing that would show up in review anyway, but easier to bake in from the start.
Go ahead with the PR
The direction is solid and the scope is well-controlled. Please open the PR — happy to review as soon as it's up. No need to break it into smaller pieces; the ~1.1k LOC as described (including tests + docs) is a coherent unit.
Also happy to dig into the DMA-mapped pinned buffer follow-up when you get to it — the RDMA-registration interaction is genuinely subtle.
@Liwink commented on GitHub (Sep 1, 2026):
Quick update: working with TPU team on long-term support for mooncake - a new, more efficient TPU library is almost to use