Skip to content

[Bugfix][TENT] Fix silent TPU data corruption for transfers larger than one staging chunk#2815

Merged
alogfans merged 1 commit into
kvcache-ai:mainfrom
Liwink:fix/tpu-interior-pointer-corruption
Jul 10, 2026
Merged

[Bugfix][TENT] Fix silent TPU data corruption for transfers larger than one staging chunk#2815
alogfans merged 1 commit into
kvcache-ai:mainfrom
Liwink:fix/tpu-interior-pointer-corruption

Conversation

@Liwink

@Liwink Liwink commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a silent data-corruption bug in the TENT TPU staging path added by #2733 (RFC #2662).

Found by running the TPU path on a real TPU VM for the first time (v5p-8, libtpu 0.0.32, PJRT C API 0.83). Everything before this had only ever run against the mock adapter.

The bug

ProxyManager stages a transfer in chunk_size (4 MiB) pieces and passes token + chunk_offset to the platform for every chunk after the first:

// mooncake-transfer-engine/tent/src/runtime/proxy_manager.cpp:76
local_stage.source = (uint8_t*)request.source + offset;

The adapter ABI only ever specified base-address classification. So for offset > 0:

  1. mc_tpu_pjrt_is_device_ptr(token + offset) returns 0,
  2. TpuPlatform::copy concludes neither side is device memory,
  3. it falls through to CpuPlatform::copy — a plain memcpy of the device token.

On real PJRT that memcpy does not crash. PJRT_Buffer_UnsafePointer returns an address that is host-readable but does not hold the buffer's data. The staging copy produced garbage and reported COMPLETED.

Net effect: chunk 0 is correct, every later chunk is silently wrong. Any TPU transfer larger than 4 MiB was corrupted, with no error raised anywhere — which is essentially every real KV-cache transfer.

Why CI never caught it

Two independent reasons, both fixed here:

  • -DUSE_TPU=ON compiled zero TPU code. All TPU sources live under tent/, which is gated on USE_TENT. The build configured and succeeded while building nothing.
  • The mock adapter's "device" pointers were ordinary host memory, so the accidental memcpy produced the right bytes. The mock structurally could not reproduce the failure.

Changes

  • tpu_pjrt_abi.h / tpu_pjrt_shim.h — specify that classification and copy entrypoints must resolve an interior address to the registered buffer whose range contains it; that a copy may not run past a buffer's end; and that the token must never be dereferenced.
  • tpu_transport.cpp — require exactly one TPU-device side per staging hop, and fail loudly otherwise. Defense in depth: a non-conforming or absent adapter can no longer degrade into a silent memcpy.
  • mock_tpu_pjrt_adapter.cpp — model the two properties that actually matter on hardware: tokens are opaque (a poisoned read-only mapping; data lives in shadow storage) and addresses may be interior. A copy that bypasses the adapter now yields 0xDD instead of accidentally passing.
  • common.cmakeUSE_TPU=ON without USE_TENT=ON is now a hard error instead of a silent no-op.

Module

  • Transfer Engine (mooncake-transfer-engine)
  • Mooncake Store (mooncake-store)
  • Mooncake EP (mooncake-ep)
  • Mooncake PG (mooncake-pg)
  • Integration (mooncake-integration)
  • P2P Store (mooncake-p2p-store)
  • Python Wheel (mooncake-wheel)
  • Common (mooncake-common)
  • Mooncake RL (mooncake-rl)
  • CI/CD
  • Docs
  • Other

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Breaking change
  • Documentation update
  • Performance improvement
  • Other

Note: the common.cmake guard is technically build-breaking for anyone passing -DUSE_TPU=ON alone — but such a build produced no TPU code, so nothing can regress.

How Has This Been Tested?

Test commands:

cmake -S . -B build -G Ninja -DUSE_TENT=ON -DUSE_TPU=ON \
  -DWITH_STORE=OFF -DWITH_STORE_RUST=OFF -DBUILD_BENCHMARK=OFF -DUSE_RDMA=ON
cmake --build build -j
ctest --test-dir build/mooncake-transfer-engine/tent

Test results:

  • 100% tests passed, 0 tests failed out of 22
  • New: tent_tpu_transport_test (6 cases) drives TpuTransport's staging hop exactly as ProxyManager issues it — local stage at an interior device offset, the mirrored remote stage, the READ direction, and the three failure modes (no device side, two device sides, non-local target).
  • New: ClassifiesInteriorDevicePointers and CopyFromInteriorOffsetMovesTheRightBytes in tent_tpu_pjrt_shim_test.

Verified the tests actually catch the bug. Reverting the fixes (exact-match registry + no transport guard) makes LocalStageCopiesFromInteriorDeviceOffset fail exactly as the hardware behaves: status COMPLETED, staging buffer filled with 0xDD. Re-applying the fixes turns it green.

Hardware findings behind this were reproduced with a standalone PJRT probe (GetPjrtApi from libtpu.soBufferFromHostBufferUnsafePointermemcpy), confirming the token is readable and holds unrelated bytes.

  • Unit tests pass
  • Integration tests pass (if applicable)
  • Manual testing done (real TPU VM, v5p-8)

Not covered: the cross-node hop. This VM is single-host (gVNIC, no RDMA), so the device↔host staging hop is exercised, not host↔remote-host.

Checklist

  • I have performed a self-review of my own code
  • I have formatted my code using ./scripts/code_format.sh
  • I have run pre-commit run --all-files and all hooks pass — see note below
  • I have updated the documentation (if applicable)
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: I have filed an RFC issue

On pre-commit: every hook passes on the touched files except cmake-format, which rewrites large amounts of unrelated code in tent/tests/CMakeLists.txt and common.cmake. Per AGENTS.md ("do not include those unrelated edits in the PR") I kept the diff minimal and left those rewrites out; happy to let pre-commit.ci autofix, or to land them separately.

Related work

Separately, hardware benchmarking turned up an architectural issue — the PJRT sub-range copy entrypoints that Platform::copy requires run ~20× slower than the whole-buffer ones (0.8 GB/s vs 18.5 GB/s D2H), capping the staging design near ~1 GB/s per chip. That is a design discussion, not a bugfix, so I'll raise it on #2662 rather than here.

AI Assistance Disclosure

  • No AI tools were used
  • AI tools were used (specify below)

Claude Code was used to run the TPU-VM investigation (building the PJRT probes that identified the token/interior-pointer behaviour), to implement the fixes, and to draft the tests. I have reviewed every changed line and can defend the change end to end.

…an one staging chunk

Caught while testing the TPU staging path on a real TPU VM (v5p-8, libtpu
0.0.32, PJRT C API 0.83) for the first time. Until now the feature had only
ever run against the mock adapter.

ProxyManager stages a transfer in chunk_size (4 MiB) pieces and passes
`token + chunk_offset` to the platform for every chunk after the first
(proxy_manager.cpp:76). The adapter ABI only ever specified base-address
classification, so `isDevicePtr(token + off)` returned false, TENT classified
TPU HBM as host memory, and TpuPlatform::copy fell through to CpuPlatform::copy
-- a plain memcpy of the device token.

On real PJRT that memcpy does not crash: PJRT_Buffer_UnsafePointer returns an
address that is host-readable but does not hold the buffer's data. The staging
copy therefore produced garbage and reported COMPLETED. Chunk 0 was correct and
every subsequent chunk was silently wrong, so any transfer over 4 MiB was
corrupted without an error anywhere.

The mock could not catch this because its "device" pointers were ordinary host
memory, so the accidental memcpy happened to produce the right bytes.

Fixes:

- tpu_pjrt_abi.h / tpu_pjrt_shim.h: state that classification and copy
  entrypoints must resolve interior addresses to the registered buffer whose
  range contains them, that a copy may not run past a buffer's end, and that the
  token must never be dereferenced.
- tpu_transport.cpp: require exactly one TPU-device side per staging hop and
  fail loudly otherwise, so a non-conforming or absent adapter can no longer
  degrade into a silent memcpy.
- mock_tpu_pjrt_adapter.cpp: model the two properties that matter -- tokens are
  opaque (a poisoned read-only mapping, data lives in shadow storage) and
  addresses may be interior. A copy that bypasses the adapter now yields 0xDD
  instead of accidentally passing.
- common.cmake: -DUSE_TPU=ON silently compiled zero TPU code, because all TPU
  sources live under tent/ which is gated on USE_TENT. This is why the bug was
  invisible to every build. Now a hard error.

Tests: two new regression tests in tent_tpu_pjrt_shim_test and a new
tent_tpu_transport_test (6 cases) that drives the staging hop the way
ProxyManager does. Verified they fail against the pre-fix code
(LocalStageCopiesFromInteriorDeviceOffset reports COMPLETED while delivering
0xDD) and pass after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support and validation for interior pointers (offsets within registered buffers) during TPU transfers. It updates the TPU/PJRT ABI, shim, and transport layer to correctly resolve and copy to/from interior addresses, preventing silent corruption during chunked staging transfers. It also enhances the mock TPU adapter to poison tokens and simulate real PJRT behavior, and adds comprehensive unit tests for both the shim and the transport layer. The feedback identifies a potential resource leak in mock_tpu_pjrt_register_device where mmap'ed memory could be leaked if the subsequent allocation of the shadow buffer throws a std::bad_alloc exception, and suggests allocating the shadow buffer first to ensure exception safety.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +128 to 139
void *mock_tpu_pjrt_register_device(size_t size, int index) {
void *token = mmap(nullptr, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (token == MAP_FAILED) return nullptr;
std::memset(token, kPoison, size);
mprotect(token, size, PROT_READ);

std::lock_guard<std::mutex> lock(g_mutex);
g_device_registry[addr] = index;
g_device_registry.push_back(Buffer{reinterpret_cast<uintptr_t>(token),
new unsigned char[size](), size, index});
return token;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In mock_tpu_pjrt_register_device, if new unsigned char[size]() throws a std::bad_alloc exception, the previously allocated token via mmap will be leaked because munmap is never called. Allocating the shadow buffer before calling mmap ensures exception safety and prevents resource leaks.

void *mock_tpu_pjrt_register_device(size_t size, int index) {
    unsigned char *shadow = new unsigned char[size]();
    void *token = mmap(nullptr, size, PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (token == MAP_FAILED) {
        delete[] shadow;
        return nullptr;
    }
    std::memset(token, kPoison, size);
    mprotect(token, size, PROT_READ);

    std::lock_guard<std::mutex> lock(g_mutex);
    g_device_registry.push_back(Buffer{reinterpret_cast<uintptr_t>(token),
                                       shadow, size, index});
    return token;
}

@staryxchen staryxchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the deep investigation and the thorough write-up. The ABI contract clarification, the transport-level guard in TpuTransport::startTransfer, the mock adapter rewrite with poisoned tokens, and the new test coverage all look excellent — approving those.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@alogfans
alogfans merged commit 130b959 into kvcache-ai:main Jul 10, 2026
29 checks passed
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.

4 participants