Skip to content

[Store] Support LOCAL_DISK replica migration during drain (Option A)#31

Closed
liangxu2000 wants to merge 57 commits into
zchuango:mainfrom
liangxu2000:feature/drainjob-local-disk
Closed

[Store] Support LOCAL_DISK replica migration during drain (Option A)#31
liangxu2000 wants to merge 57 commits into
zchuango:mainfrom
liangxu2000:feature/drainjob-local-disk

Conversation

@liangxu2000

@liangxu2000 liangxu2000 commented Jul 23, 2026

Copy link
Copy Markdown

Description

Problem

When a Mooncake Store node is drained (decommission / scale-in), the drain
scheduler (ScheduleDrainJobTasks) migrates all replicas to a target node.
However, LOCAL_DISK replicas (client-side SSD offload files written by
FileStorage) were silently skipped because Replica::get_segment_names()
returned an empty vector for the LOCAL_DISK type. Any data that had been
evicted from memory and offloaded to the local SSD was left behind on the
decommissioned node and effectively lost to the cluster. This is issue
#2817.

PR #2834 (Step 1)
previously added a fail-closed safety net that prevents a segment from being
marked DRAINED while LOCAL_DISK residuals exist. This PR (Step 2) adds the
actual migration capability so those residuals are moved rather than merely
blocking completion.

Solution — Targeted File Transfer (reuse the existing Move flow)

Core insight.
A LOCAL_DISK replica is just a file on the source client's
local SSD. To migrate it, we need to (a) read that file into a memory buffer,
then (b) RDMA-write the buffer into a MEMORY segment on the target node.
Mooncake already has battle-tested primitives for both steps:
BatchLoad (O_DIRECT file → aligned memory) and TransferWrite (registered
memory → remote segment). The promotion path (ProcessPromotionTasks) uses
exactly this pattern within a single node; we simply redirect the
TransferWrite destination to a remote target allocated by the drain
scheduler.

Why reuse the Move flow instead of a new mechanism?
The existing drain
infrastructure (ScheduleDrainJobTasksCreateMoveTask → client executes
transfer → MoveEnd finalizes metadata) already provides:

  • per-key retry with configurable kMaxDrainUnitRetries
  • concurrency control (max_concurrency)
  • completion tracking (MaybeCompleteDrainJob)
  • segment state machine (OK → DRAINING → DRAINED)

Building a separate “LOCAL_DISK drain” path would duplicate all of this. By
plugging into the existing Move flow, we get it for free with minimal blast
radius.

Data flow (per key):

Source client                              Target client
───────────                              ─────────────
SSD file (bucket storage)
    │
    │ BatchLoad (O_DIRECT, align_down/align_up)
    ▼
Staging buffer (4096-aligned,
  in registered memory region)
    │
    │ TransferWrite (RDMA)
    └───────────────────────────▶  Newly allocated MEMORY replica
                                   (in target's mounted segment)

Implementation steps:

  1. Visibility. Replica::get_segment_names() now returns the
    transport_endpoint for LOCAL_DISK replicas. Previously it returned an
    empty vector, making them invisible to the scheduler.

  2. Scheduling with MEMORY-first dedup. ScheduleDrainJobTasks scans all
    replicas on the draining segment. If a key has both a MEMORY and a
    LOCAL_DISK replica on the same client, only the MEMORY replica is
    scheduled for migration (it is already in RAM — cheaper to transfer). The
    redundant LOCAL_DISK metadata is cleaned up in MoveEnd (see step 5).
    Only keys that are LOCAL_DISK-only (memory was already evicted) go
    through the file-transfer path.

  3. File transfer branch. ExecuteReplicaTransfer gains a LOCAL_DISK
    source branch:

    • LoadBatchFromLocalDisk calls AllocateBatch (allocates an
      O_DIRECT-aligned staging buffer in registered memory) + BatchLoad
      (reads the SSD file via align_down/align_up offset correction).
    • TransferWrite RDMA-writes the staging buffer into the target's freshly
      allocated MEMORY replica.
    • A replica_move_local_disk_transfer_success log is emitted on success,
      making the path observable and testable.
  4. is_drain flag (see Design notes). A boolean is threaded from the
    scheduler through ReplicaMovePayloadClient::MoveMoveEnd RPC.

  5. Conditional metadata cleanup. When a drain move relocates a MEMORY
    replica, the same key's LOCAL_DISK replica on the source client is now
    redundant (it is a disk backup of the memory data we just moved, and the
    target already holds that data). MoveEnd drops the LOCAL_DISK master
    metadata only when is_drain=true. The physical SSD file is
    intentionally left on disk as a safety net (no destructive deletion);
    physical cleanup is a separate future step.

  6. Completion guard. MaybeCompleteDrainJob checks for LOCAL_DISK
    residuals before marking a segment DRAINED. If any LOCAL_DISK-only
    keys remain (e.g. because they failed), the segment stays in DRAINING
    and the job is marked FAILED.

What is NOT introduced: no new RPC, no new master state machine, no new
task type. The entire feature plugs into the existing Move pipeline.

Testing

5 end-to-end tests drive real eviction-driven offload, manual moves, and
failure injection, verifying the full per-key data route with byte-for-byte
comparison and direct log evidence that the LOCAL_DISK transfer path
(LoadBatchFromLocalDisk → staging buffer → TransferWrite) is exercised.

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

Changes

File Change
include/replica.h get_segment_names() returns the transport_endpoint for LOCAL_DISK so the drain scheduler can locate them.
src/client_service.cpp ExecuteReplicaTransfer: new LOCAL_DISK source branch (SSD -> staging buffer via LoadBatchFromLocalDisk -> TransferWrite -> target MEMORY). Adds a ..._local_disk_transfer_success log so the path is observable/testable.
src/file_storage.cpp LoadBatchFromLocalDisk (AllocateBatch + BatchLoad) exposed for the client.
src/master_service.cpp CreateMoveTask: client fallback for LOCAL_DISK sources, plus an is_drain flag (default false; drain sets it true). ScheduleDrainJobTasks: scans LOCAL_DISK replicas with MEMORY-first dedup. MaybeCompleteDrainJob: checks LOCAL_DISK residue before marking a segment DRAINED. MoveEnd: only for drain moves (is_drain=true) drops the redundant LOCAL_DISK metadata after a MEMORY move, with a scope-narrowing fix for an EDEADLK on segment_mutex_ (unique -> shared on the same std::shared_mutex).
include/task_manager.h, include/client_service.h / src/client_service.cpp, include/master_client.h / src/master_client.cpp, include/rpc_service.h / src/rpc_service.cpp Thread an is_drain flag from the drain scheduler through the move task payload (ReplicaMovePayload), the client (Client::Move / ExecuteTask), and the MoveEnd RPC chain, so MoveEnd can distinguish drain moves from the public create_move_task API. Manual moves default to is_drain=false.
include/real_client.h / src/real_client.cpp mountLocalDiskSegment() + getOffloadRpcAddr() (used by the e2e test to register a LOCAL_DISK segment).
include/master_config.h / tests/test_server_helpers.h InProcMasterConfig / builder gain offload_on_evict, passed through to WrappedMasterServiceConfig (test infra).
tests/e2e/CMakeLists.txt Registers local_disk_drain_test.
tests/e2e/local_disk_drain_test.cpp New end-to-end tests (see below).

Design notes

  • is_drain flag — distinguishing drain moves from manual moves. The
    public create_move_task API (used for load-balancing) and the internal
    drain scheduler both go through the same MoveMoveEnd code path.
    Without a distinguishing flag, MoveEnd would unconditionally drop the
    source client's LOCAL_DISK metadata after any MEMORY move — silently
    discarding a durable SSD backup on a still-live node during a manual move.
    To prevent this, a boolean is_drain is threaded through:
    ScheduleDrainJobTasks sets it true in ReplicaMovePayload
    task_manager.h carries it in the task → Client::Move / ExecuteTask
    forwards it → MasterClient::MoveEnd passes it in the RPC →
    MasterService::MoveEnd gates the LOCAL_DISK metadata removal on
    is_drain == true. Manual moves default to is_drain=false, preserving
    the source's LOCAL_DISK replica.
  • MEMORY-first dedup (drain moves only). When a key has both a MEMORY
    and a LOCAL_DISK replica on the draining client, only the MEMORY replica
    is migrated; the LOCAL_DISK metadata is dropped in MoveEnd (the SSD file
    is intentionally retained on disk as a safety net — physical cleanup is
    future work).
  • Eviction watermark is GLOBAL. get_global_mem_used_ratio() is
    allocated / capacity summed across all mounted segments. Mounting a
    large target segment therefore dilutes the ratio and can stop eviction. The
    test mounts the drain target only after eviction has freed memory (see
    test scenarios).
  • offload_on_evict (easy to confuse with enable_offload). It is gated
    by enable_offload: offload_on_evict_ = enable_offload_ && config.offload_on_evict, so it can only take effect when offload is enabled.
    It changes when offload happens, not whether:
    • offload_on_evict=false (default): every PutEnd offloads to SSD, so a key
      already has a LOCAL_DISK replica by the time eviction runs.
    • offload_on_evict=true: PutEnd does not offload; a key is queued for
      offload only when eviction picks it and finds no LOCAL_DISK replica yet,
      which yields a two-step eviction (queue offload + pin, then drop MEMORY in
      the next cycle once the disk backup exists).

How Has This Been Tested?

Test commands:

cd build
cmake --build . --target local_disk_drain_test -j$(nproc)
GLOG_logtostderr=1 ./mooncake-store/tests/e2e/local_disk_drain_test

Test scenarios (5 tests, all pass):

  1. LocalDiskDrainTest.DrainLocalDiskReplicasToPeer
    Purpose: verify the baseline happy path — drain correctly migrates data
    when every key has both MEMORY + LOCAL_DISK replicas, using the MEMORY-first
    dedup policy.
    Setup: 4 keys x 4 KB on client1 (MEMORY + LOCAL_DISK); drain client1 →
    client2. Asserts: drain SUCCEEDED, MEMORY replica migrated (dedup),
    LOCAL_DISK metadata dropped, all keys byte-for-byte readable on client2.

  2. LocalDiskDrainTest.ManualMovePreservesLocalDiskBackup
    Purpose: regression guard for the is_drain gating — prove that a
    manual move (is_drain=false) does NOT drop the source's LOCAL_DISK
    replicas, and that a subsequent drain correctly uses the LOCAL_DISK transfer
    path (including non-aligned O_DIRECT reads).
    Setup: manually move each key's MEMORY replica client1 → client2 via the
    public create_move_task API, then drain client1 → client3. Keys use a mix
    of 4096-aligned and deliberately non-aligned sizes ({4096, 7001, 12000,
    5000}) filled with a position-dependent pattern. Asserts: all 4 keys
    migrated via LOCAL_DISK path (local_disk_transfer_success count),
    byte-for-byte correct on client3 (guards the O_DIRECT aligned-read offset
    fix in BatchLoad). If the manual move had wrongly dropped LOCAL_DISK
    metadata, succeeded_units would be 0.

  3. LocalDiskDrainTest.DrainFailsGracefullyWhenLocalDiskDataMissing
    Purpose: verify graceful failure handling — when the source SSD files
    are missing/corrupt, the drain job must reach a terminal FAILED state
    (no hang), correctly account retries, and NOT corrupt surviving replicas.
    Setup: put 4 keys on client1 (MEMORY + LOCAL_DISK), manually move MEMORY
    to client2 (leaving client1 LOCAL_DISK-only), delete client1's SSD files,
    then drain client1 → client3. Asserts: job FAILED (not stuck),
    succeeded_units == 0, local_disk_load_failed error branch hit,
    source segment NOT marked DRAINED, client2's MEMORY copy byte-for-byte
    intact. Exercises the retry / terminal-failure accounting
    (kMaxDrainUnitRetries = 3).

  4. LocalDiskDrainEvictTest.DrainAfterEviction_OffloadOnEvict
    Purpose: verify drain works end-to-end in a realistic eviction scenario
    with offload_on_evict=true (two-step eviction), where some keys are
    MEMORY-only and some are LOCAL_DISK-only at drain time.
    Setup: 16 keys x 4 MB filling a 64 MB segment; eviction offloads to SSD
    and frees MEMORY for ~8 keys. Asserts: drain migrates ~8 keys via MEMORY
    path and ~8 keys via LOCAL_DISK path; all 16 keys byte-for-byte readable
    on target.

  5. LocalDiskDrainEvictTest.DrainAfterEviction_ImmediateOffload
    Purpose: same as above but with offload_on_evict=false (offload at
    PutEnd), verifying that both offload modes produce a correct drain outcome.
    Setup: identical topology; eviction frees MEMORY for ~8 keys. Asserts:
    drain again exercises both MEMORY and LOCAL_DISK paths successfully.

Test results:

  • Unit tests pass
  • Integration tests pass (e2e local_disk_drain_test, 5/5 PASSED)
[==========] 5 tests from 2 test suites ran.
[  PASSED  ] 5 tests.

Log evidence (what you see when running the tests):

The tests emit structured glog output (GLOG_logtostderr=1) and use a
CapturingLogSink to programmatically assert that specific code paths were
taken. Here is what the key evidence looks like:

Test 2 — LOCAL_DISK transfer path exercised (success):

I0723 ... action=replica_move_local_disk_transfer_success, key=manual_move_key_0, size=4096
I0723 ... action=replica_move_local_disk_transfer_success, key=manual_move_key_1, size=7001
I0723 ... action=replica_move_local_disk_transfer_success, key=manual_move_key_2, size=12000
I0723 ... action=replica_move_local_disk_transfer_success, key=manual_move_key_3, size=5000

This proves the full SSD → staging buffer → TransferWrite → target MEMORY
path ran for every key, including non-aligned sizes (7001, 12000, 5000 bytes)
that exercise the O_DIRECT align_down/align_up offset correction in
BatchLoad. The test asserts this count >= 4.

Test 3 — graceful failure when SSD files are deleted (4 keys × 3 retries):

W0723 ... LoadBatchFromLocalDisk: BatchLoad failed for key=fail_key_0, error=FILE_OPEN_FAIL
E0723 ... error=local_disk_load_failed, error_code=FILE_OPEN_FAIL, key=fail_key_0
W0723 ... LoadBatchFromLocalDisk: BatchLoad failed for key=fail_key_0, error=FILE_OPEN_FAIL
E0723 ... error=local_disk_load_failed, error_code=FILE_OPEN_FAIL, key=fail_key_0
W0723 ... LoadBatchFromLocalDisk: BatchLoad failed for key=fail_key_0, error=FILE_OPEN_FAIL
E0723 ... error=local_disk_load_failed, error_code=FILE_OPEN_FAIL, key=fail_key_0
... (same pattern for fail_key_1, fail_key_2, fail_key_3)

Each key fails exactly 3 times (kMaxDrainUnitRetries = 3), producing 12
local_disk_load_failed lines total. The test asserts:

  • status_name == "FAILED" (job reached terminal state, no hang)
  • succeeded_units == 0 (nothing was migrated)
  • source segment NOT marked DRAINED
  • client2's surviving MEMORY copy is byte-for-byte intact (the failed drain
    did not corrupt or drop the live replica)

Tests 4/5 — eviction-driven drain (final job query):

drain job status: SUCCEEDED
succeeded=16  failed=0  blocked=0  migrated_bytes=67108864

All 16 keys (8 via MEMORY path + 8 via LOCAL_DISK path) migrated successfully;
migrated_bytes = 16 × 4 MB = 64 MiB confirms no data was silently dropped.

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
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: I have filed an RFC issue

RFC note: the design is documented in drain-local-disk-migration-rfc.md
(Option A vs Option C, follow-up to kvcache-ai#2817 / kvcache-ai#2834). The non-test source change
is under 500 LOC; please confirm whether a separate GitHub RFC issue is still
required before merge.

AI Assistance Disclosure

  • AI tools were used

AI coding assistants were used to help implement the feature, write the tests,
diagnose test behavior, and review the change. The human submitter has reviewed
the changes and is responsible for defending them end-to-end.

dependabot Bot and others added 30 commits July 4, 2026 23:01
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.55.0.
- [Commits](golang/net@v0.47.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* [TE] Add TPU (PJRT) staging support to TENT

Adds Google TPU support to the TENT transfer engine. TPU HBM is not
NIC-addressable, so transfers touching TPU memory are staged through host
DRAM and chained by the existing ProxyManager pipeline:

    TPU HBM <-> host DRAM (PJRT device copy)  ->  host <-> host (RDMA/TCP)

Rather than a standalone networked transport, this reuses ProxyManager's
chunked double-buffering / staging machinery and adds only:

  * MTYPE_TPU memory type + "tpu" location/type parsing.
  * TpuPlatform (derives CpuPlatform): host DRAM + NIC topology are
    inherited; only the TPU-device-aware paths (copy, getMemoryType,
    getLocation, per-device MemEntry probe) are overridden and delegated
    to a device-copy adapter.
  * A thin TpuTransport: the local HBM<->host staging executor. It only
    advertises gpu_to_dram / dram_to_gpu (gpu_to_gpu stays false so the
    engine always stages cross-node traffic through host DRAM) and runs
    the copy via Platform::copy for LOCAL_SEGMENT_ID requests. It is
    same-machine-only, like SHM/NVLINK.
  * A findStagingPolicy case for TPU: local HBM<->host via TpuTransport,
    host<->host via whichever host transport is present (RDMA or TCP;
    cloud TPU deployments are typically TCP/multi-NIC).

The PJRT device I/O (HBM<->host DMA, pointer classification, device
topology) sits behind TpuPjrtShim, which resolves an adapter shared
library at runtime via dlopen (C ABI in tpu_pjrt_abi.h). TENT therefore
carries no build-time PJRT/XLA dependency. The whole feature is gated
behind -DUSE_TPU (OFF by default), so CI and existing builds are
unaffected.

Includes a mock adapter + unit test that exercise the shim ABI on any
Linux host without TPU hardware, and a docs entry under supported
protocols.

Refs kvcache-ai#2662.
…in auto-selection (kvcache-ai#2741)

* [TransferEngine] Prefer private-range IPv4 GIDs over link-local IPv6 in auto-selection

Fixes kvcache-ai#2729.

isOverlayIPv4() classifies every RFC1918/CGNAT address as overlay, which
dropped routable 10.x datacenter-fabric GIDs into the same degraded tier
as link-local fe80:: GIDs; the lowest-gid-index tie-break then
deterministically picked the link-local entry (indices 0/1 on mlx5),
which can only ever work same-L2 and broke RoCEv2 deployments addressed
from 10/8.

Split the degraded tier instead of reordering anything else: private-range
IPv4-mapped GIDs (10/8, 172.16/12, 100.64/10) now rank in their own tier
strictly below genuinely routable GIDs and strictly above link-local /
overlay-named-interface GIDs. The interface-name overlay heuristic
(docker*/cni*/...) remains the strongest demotion signal. All pre-existing
cross-tier orderings are preserved; deployments that pin MC_GID_INDEX are
unaffected. For the topology reported in kvcache-ai#2729 this restores the effective
pre-4d7c1a19 selection.
…ai#2734)

When aclrtPointerGetAttributes returns an unknown location type, the
transport already falls back to host memory; INFO is sufficient and
avoids noisy error logs during normal operation.

Co-authored-by: lbjyx <youxiao@huawei.com>
…kvcache-ai#2714)

* [TENT] Fix data race on local SegmentDesc via copy-on-write snapshots

Writers (SegmentTracker add/remove, transport install paths) mutated the
local SegmentDesc in place while readers (GetSegmentDesc JSON dump,
findBuffer in transports and route resolution) accessed it without any
locking — undefined behavior under concurrent register/unregisterLocalMemory
(issue kvcache-ai#2477).

The local desc is now published as immutable copy-on-write snapshots:
- SegmentManager::updateLocal() serializes all mutations behind a writer
  mutex, applies them to a private clone and atomically publishes it.
- getLocal() returns a consistent snapshot; readers may observe a stale
  snapshot, never a torn one. Staleness matches the existing metadata
  semantics (remote descs are cached with TTL + best-effort invalidation).
- Sites that retain BufferDesc* past their lookup scope now pin the
  snapshot (RouteHint::pin, withCachedSegment pin overload).
- The serialized-JSON cache is version-tagged so a slow dump of an old
  snapshot can never overwrite the cache entry of a newer publication.
- SegmentTracker::query() (unused, returned raw pointers into the buffer
  vector) is removed.

Adds segment_manager_test with a writers-vs-readers regression test that
fails under the previous in-place mutation (TSAN data race + torn-entry
invariant violations).

* [TENT] Address review: release/acquire on version counter, addInBatch rollback

- Publish local_desc_version_ with memory_order_release (pairs with the
  acquire load in getLocal): per [atomics.order] a relaxed bump creates no
  synchronizes-with edge, so on weakly-ordered targets a reader could tag
  the old snapshot with the new version and its thread-local cache would
  serve the stale snapshot until the next publication. Same reasoning for
  the getLocalDumpedJson tag sample.
- Roll back duplicate ref_count bumps when the addInBatch registration
  callback fails, so a failed registration no longer pins buffers forever
  (pre-existing behavior on main). The bump itself stays *before* the
  callback: holding ref_count >= 2 under the writer mutex is what prevents
  a concurrent unregister from erasing the entry between the duplicate
  check and the commit.
- Regression test for the rollback path.

* [TENT] Rebase adaptation: pin snapshots in kvcache-ai#2691's batch-submit paths

kvcache-ai#2691 (merged after this branch was cut) added two
segment_manager.getLocal().get() sites in the NVLink/MNNVL batch submit
paths. Under the copy-on-write snapshot semantics introduced here, that
raw pointer is only guaranteed valid while an owning SegmentDescRef is
held; hold the reference for the duration of the batch validation loop.
Co-authored-by: Aionw <aionw@users.noreply.github.com>
…vcache-ai#2751)

In findStagingPolicy the "pure mnnvl" branch was guarded by
transport_list_[MNNVL] && transport_list_[NVLINK] but read
capabilities() from transport_list_[RDMA]. When RDMA is absent (the
actual "pure mnnvl" case this branch is meant to serve),
transport_list_[RDMA] is a null shared_ptr and calling capabilities()
on it dereferences null. Even with RDMA present, using RDMA's caps to
drive MNNVL staging decisions is semantically wrong.

Read caps from transport_list_[MNNVL], matching the guard and the
pattern already used by case 1 (RDMA) and case 3 (TPU).

Signed-off-by: staryxchen <staryxchen@tencent.com>
…#2732)

Co-authored-by: Aionw <aionw@users.noreply.github.com>
…ai#2568 step 1) (kvcache-ai#2640)

* [TENT] SelectionPolicy: add per-policy SL/TC/qp_pool schema (RFC kvcache-ai#2568 step 1)

Step 1 of the two-step plan agreed with @staryxchen on kvcache-ai#2568: extend the
existing `SelectionPolicy` (the opaque `policy_name` hook, not a new enum)
with link-layer QoS attributes, so fabric QoS can be configured per policy
instead of only process-globally via MC_IB_SL / MC_IB_TC.

This is schema + plumbing only. It does NOT yet apply SL/TC at QP setup —
real per-class differentiation needs per-class QP pools, which is the
explicit step-2 follow-up. `qp_pool` is parsed and reserved here so the
JSON schema is forward-compatible and step 2 needs no schema change or
breakage of existing configs (per @staryxchen's reminder).

- SelectionPolicy: add optional `service_level` (0-15), `traffic_class`
  (0-255), `qp_pool` (string). nullopt = fall back to the global RdmaParams
  default = today's behavior.
- loadPolicies(): parse the three from the same JSON as existing fields;
  out-of-range SL/TC are ignored with a warning so a bad config never
  changes selection.
- SelectionResult: carry the matched policy's SL/TC/qp_pool out to the
  caller for the step-2 QP-setup follow-up.
- Tests: parse + carry-through, and out-of-range-ignored.

Fully backward compatible: unset fields behave exactly as before, and the
default policies (which don't set them) are unaffected.

Note: not built locally (no TENT toolchain on my dev machine); relying on
CI. clang-format (v20.1.8) applied.

* [TENT] Address review: qp_pool empty/non-string handling + trim comments

Per @staryxchen's review on kvcache-ai#2640:
- Treat an empty-string qp_pool the same as unset (nullopt = default pool),
  so a blank config value doesn't look like an explicit pool.
- Warn on a non-string qp_pool (e.g. `"qp_pool": 42`) instead of silently
  no-op'ing it, matching the SL/TC out-of-range warnings — a typo'd config
  should not look like it took effect.
- Trim the RFC-reference / step-split comments in the header.
- Add a test covering empty / non-string qp_pool -> unset.

---------

Co-authored-by: catyans <catyans@users.noreply.github.com>
…-ai#2722) (kvcache-ai#2754)

* [TENT] Fix cross-GPU NVLink/MNNVL event-stream device mismatch (kvcache-ai#2722)

Fix 'cudaEventRecord: invalid resource handle' error when NVLink/MNNVL
transfers target a GPU different from the current CUDA device.

Root cause: CUDAStreamPool creates streams on the target device but
restores the caller's original device afterward. Later, cudaEventCreate
and cudaEventRecord execute on the caller's device, creating a device
mismatch between the event and the stream.

Solution:
- Store the stream's device ID in NVLinkSubBatch/MnnvlSubBatch
- Wrap event creation and recording in a device guard that switches to
  the stream's device
- Restore the original device on all paths (success and error)

This fix ensures events and streams are created on the same device,
resolving cross-GPU transfer failures while maintaining correct device
context for the caller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix CHECK_CUDA macro usage in void functions

Replace CHECK_CUDA with manual error handling in startTransfer functions
to fix compilation errors. CHECK_CUDA is designed to return Status objects,
but startTransfer has void return type.

Changes:
- Manually check cudaGetDevice/cudaSetDevice errors in device switching
- Log errors and mark tasks as FAILED on failure
- Call cudaSetDevice directly without CHECK_CUDA when restoring device

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Apply clang-format to fix code formatting violations

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ti-protocol segments (kvcache-ai#2753)

* [TE] Route cross-host targets over rdma automatically in rdma+hip segments

A "rdma,hip" multi-protocol segment registers the device KV pool under both
rdma and hip. hip transport uses GPU IPC, which only works between processes
on the same physical host, so selecting hip for a target on another host makes
the initiator call hipIpcOpenMemHandle on a remote GPU handle and fail with
"Error code 17 - invalid device pointer".

Until now the only mitigation was the MC_DISABLE_HIP env, a global switch that
also disables the intra-node hip fast path. Add an automatic same-host locality
gate instead: compare the host portion of the target segment name against the
local server name and skip hip buffers for cross-host targets so they fall back
to rdma. Intra-node targets still use hip. mp_selectTransport mirrors the same
downgrade for an explicit hip preference.

The locality helpers are factored into multi_transport_locality.h with a unit
test (multi_transport_locality_test), matching the rdma_gid_probe pattern.
MC_DISABLE_HIP remains as a manual override for backward compatibility.

* Make hip locality gate IPv6-safe and case-insensitive

segmentHost now parses bracketed [v6]:port, bare IPv6 literals
(multi-colon, no port -> whole string is the host), and the plain
host:port / hostname forms. Host comparison is case-insensitive
(hostEquals), since DNS names and IPv6 hex literals may differ only
in letter case.

mp_selectTransport now downgrades a cross-host hip preference to rdma,
then tcp, and returns NotSupportedTransport when neither is offered,
instead of silently keeping hip when rdma is absent.

Adds SegmentHostParsesIPv6, HipReachableForIPv6, and
HostMatchIsCaseInsensitive tests.
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.38.0 to 0.55.0.
- [Commits](golang/net@v0.38.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#2720)

* [CI/Build] Add dedicated tent-ci job with CUDA/CPU matrix

Extract TENT build/test into a dedicated job with a USE_CUDA=ON /
USE_CUDA=OFF matrix, gate it on transfer-engine/common/CMake path
changes, and require it in ci-gate.

Signed-off-by: staryxchen <staryxchen@tencent.com>

* [CI/Build] Skip TENT tests on cuda-on leg (build-only)

GitHub runners have no real GPU; with USE_CUDA=ON tent's cuda_probe
hits the CUDA stub driver at runtime and drives dispatch past the
fake objects the unit tests rely on, causing 6 tests in 4 binaries
to fail. Restrict Test (TENT) to the cuda-off leg. cuda-on still
compiles every #ifdef USE_CUDA branch.

Signed-off-by: staryxchen <staryxchen@tencent.com>

---------

Signed-off-by: staryxchen <staryxchen@tencent.com>
Added details about LightX2V's support for disaggregated deployment and linked to the relevant blog post.
…he-ai#2649)

Co-authored-by: 郭祥 <kuiper@vivo.com>
Co-authored-by: Claude <noreply@anthropic.com>
…step 2) (kvcache-ai#2759)

* [TENT] Per-pool QP allocation with per-pool SL/TC (RFC kvcache-ai#2568 step 2)

Step 1 (kvcache-ai#2640) added the SelectionPolicy `qp_pool` schema (parsed and
carried, but not yet wired). This step makes named QP pools real at the
RDMA link layer: each pool gets its own contiguous run of data QPs inside
every endpoint, handshaked with that pool's Service Level / Traffic Class.
This is the "allocate multiple QPs, assign different params during their
handshakes" mechanism @alogfans described on the RFC.

- EndPointParams::qp_pools: optional per-pool layout (name, num_qp, SL, TC).
  Empty (default) = today's single homogeneous run of qp_mul_factor QPs.
- construct(): total QP count = sum of per-pool num_qp when pools are set;
  qp_pool_segments_ records each pool's [begin, begin+num_qp) span.
- setupOneQP(): a QP in a pool that overrides SL/TC handshakes with the
  pool's values; otherwise falls back to the global endpoint SL/TC.
- rdma_transport: parse `transports/rdma/endpoint/qp_pools` from config.
  Pool SL/TC live in the RDMA config (link-layer concept); SelectionPolicy
  only references a pool by name.

Wire-compatible: BootstrapDesc is unchanged. Both peers derive the same
pool layout from the same config, so the flat qp_num list still pairs
positionally. Default path (no pools configured) is byte-for-byte the
prior behavior.

Slice-to-pool routing (feeding SelectionResult.qp_pool into QP selection)
is a follow-up so this stays reviewable in small steps.

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

* [TENT] Route slices to their QP pool (RFC kvcache-ai#2568 step 3)

Step 2 built per-pool QP segments with per-pool SL/TC. This step wires the
SelectionResult.qp_pool (from step 1's schema) all the way down so transfers
actually land on their pool's QPs:

  SelectionResult.qp_pool -> TaskInfo.qp_pool -> SubBatch.qp_pool
    -> RdmaTask.qp_pool -> submitSlices() picks a QP inside that pool's
       segment (selectQpInPool)

- selectQpInPool(): pure router. A named, known pool folds the worker-lane
  candidate into that pool's [begin, begin+num_qp) span; empty/unknown pool
  or no pools configured => unchanged global spray. Result always in range.
- submitSlices(): all slices in a list share one task/pool, so it reads
  slice->task->qp_pool once and routes accordingly.
- qp_pool is carried like the existing device_mask, through the same task/
  sub-batch plumbing, so the flow mirrors code reviewers already know.

Default path (no qp_pool on the policy) is unchanged: pool_name is empty,
selectQpInPool returns candidate % qp_count -- the prior behavior.

Tests: SelectQpInPoolTest (6 cases) covers empty/unknown/named pools,
no-segments fallback, negative-candidate clamping, and in-range invariant.

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

* [TENT] Reject QP pool layout when any pool has non-positive num_qp

Addresses review on kvcache-ai#2759: a pool with num_qp <= 0 would produce an empty or
negative [begin, begin+num_qp) span and break the router. computeQpPoolSegments
now rejects the whole layout (valid=false, falls back to the default single
homogeneous pool) instead of building a broken one. Adds zero/negative unit
tests; qp_pool_layout_test passes 12/12.

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

* ci: retrigger flaky build (3.10); no code change

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…EXCLUDE) (kvcache-ai#2760)

* [TENT] Wire MC_TE_FILTERS(_EXCLUDE) env to the NIC allow/deny filter

The platform probes already support NIC allow/deny lists via the config keys
`topology/rdma_whitelist` / `topology/rdma_blacklist` (see filterInfiniBandDevices
in the platform probes), but there was no environment variable to set them, so
users had no way to restrict which RDMA NICs the engine discovers and uses.

The legacy Transfer Engine already exposes a device whitelist via `MC_TE_FILTERS`
(comma-separated). This reuses the *same* env var and semantics for the TENT
path so a single variable scopes NICs across both engines, and adds
`MC_TE_FILTERS_EXCLUDE` as a deny-list (the legacy engine has an allow-list
only).

On multi-NIC / multi-NUMA hosts this matters: the rail selection enumerates
every discovered remote device and will attempt QPs to devices that are not
reachable from the peer, which fail at modify-QP-to-RTR and drag down
aggregate throughput (see kvcache-ai#2758; related mapping issue kvcache-ai#2467).

This adds:
- setArrayConfig(): parse a comma-separated env value into a string array
  (trims whitespace, drops empty items), reusing the approach of parseDoubleArray.
- MC_TE_FILTERS         -> topology/rdma_whitelist (allow-list, takes precedence)
- MC_TE_FILTERS_EXCLUDE -> topology/rdma_blacklist (deny-list)
- Unit tests for both mappings, whitespace trimming, and the unset default.
- Env var docs.

Opt-in: unset = discover all NICs (unchanged default behavior).

Measured on an 8x H20 / CX-7 200G RoCEv2, 4-bond dual-NUMA box (2 nodes,
TENT backend): without the filter, cross-node transfer hits repeated
`modify QP to RTR error 22` on the NUMA1 bonds and runs at 0.14 GB/s;
with `MC_TE_FILTERS=mlx5_bond_0` the topology is scoped to the reachable
NIC, the errors disappear, and throughput recovers to 48 GB/s.

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

* ci: retrigger flaky tent-ci (cuda-off); no code change

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Michael Goin <mike.goin12@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
kvcache-ai#2739)

Previously hardcoded to 0, now configurable with default "0" to preserve existing behavior.

Signed-off-by: tan changzhi <544463199@qq.com>
…kvcache-ai#2519 step 2) (kvcache-ai#2763)

* [TENT] Opt-in earliest-deadline-first dispatch in admission queue (RFC kvcache-ai#2519 step 2)

Step 1 (kvcache-ai#2618) added Request.deadline_ns + post-hoc MLU observability. This
adds the first policy step alogfans green-lit: an opt-in EDF ordering in
LocalTransferAdmissionQueue::pickForDispatch.

- QueueLimits gains `deadline_aware` (default false).
- When false: pickForDispatch keeps strict FIFO — behavior unchanged.
- When true: the dispatch queue is stably reordered earliest-deadline-first
  before selection; owners without a deadline (deadline_ns == 0) keep FIFO
  order behind all deadlined owners.
- Selection still respects the existing owner/byte capacity limits; this only
  reorders *which* queued owner is picked next, it does not admit or reject.

No codec / local-decode / bandwidth-prediction here — this is the ordering
layer only, strictly additive and gated behind the opt-in flag.

Motivation from measurement (H20 / CX-7 RoCEv2, TENT backend): sweeping the
deadline shows a transition band (~200us on this HW) where feasible and
missed transfers coexist (mean MLU 1.31, ~13% feasible) — exactly where EDF
ordering has leverage. Below/above that band ordering buys nothing. Misses
there are driven by concurrency queueing, which is what this reordering
targets.

Adds 3 unit tests (EDF order, undeadlined-last, and FIFO-unchanged default);
full admission_queue_test suite passes (16/16).

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

* [TENT] EDF: order fifo_ at admission instead of re-sorting every dispatch

Addresses review on kvcache-ai#2763: pickForDispatch used to std::stable_sort the whole
fifo_ (up to max_outstanding_owners, default 1024) on every call, and it is
called on every complete/poll/submit — re-sorting an already-ordered queue
repeatedly, even when nothing new was admitted, and even when a byte limit
lets it consume only a few entries.

Instead keep fifo_ EDF-ordered as owners arrive: when deadline_aware, tryAdmit
inserts each owner at its earliest-deadline-first position (upper_bound, so
same-deadline owners keep FIFO tie-break — identical ordering to the old stable
sort). pickForDispatch then just consumes from the front, dropping the sort
entirely. Hot dispatch path goes from O(N log N) to O(picked); the cost moves
to one O(N) ordered insert per admit. Default (deadline_aware == false) is
unchanged plain FIFO push_back.

Adds a test admitting out-of-order deadlines across separate tryAdmit calls to
cover the ordered-insert path; full admission_queue_test passes.

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

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…kvcache-ai#2780)

* [Bench] Enable replay speedup and multi-threading

* Correct multithreading

* Update benchmarks/storage_benchmark_v1/benchmark.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Change multi-thread benchmarking

* Add notes

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
catyans and others added 17 commits July 10, 2026 10:14
…vcache-ai#2808)

* [TENT] Expose Request.deadline_ns and policy_name to Python bindings

The C++ Request struct already carries deadline_ns (RFC kvcache-ai#2519, kvcache-ai#2618)
and policy_name (kvcache-ai#2640/kvcache-ai#2759), but the Python bindings only exposed
priority and transport_hint. This left deadline / policy as
internal-only fields, unreachable from the Python API that upper
layers (Store, SGLang, vLLM) use.

Expose both as optional constructor args (defaulting to existing
behavior: deadline_ns=0, policy_name=None) and as read/write
properties. Fully backward compatible: callers that omit the new args
are unchanged.

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

* fix: include optional for pybind request fields

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Yanshu <237344440@qq.com>
…an one staging chunk (kvcache-ai#2815)

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>
* clear legacy benchmark results

* Change vllm performance benchmark docs menu

* rename vllm performance file name

* Change Mooncake performance docs paths

* Change SGLang performance docs paths

* update the sglang and vllm performance description

---------

Co-authored-by: Ke Yang <yangke@approaching.ai>
…kvcache-ai#2803)

Signed-off-by: Csrayz <33659823+Csrayz@users.noreply.github.com>
…anager (kvcache-ai#2805)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-ai#2810)

* [TENT] Add IntentType enum to Request for Transfer Intent API

Define standard intent categories (FOREGROUND_GET, BACKGROUND_PREFETCH,
MIGRATION, CHECKPOINT, WEIGHT_LOADING, STAGING_INTERNAL) so TENT can
identify a request's business semantics before scheduling.

Changes:
- types.h: add IntentType enum class + Request::intent_type field
  (default INTENT_UNSPEC, behavior byte-identical to today)
- pybind.cpp: export IntentType enum, add intent_type/policy_name/
  deadline_ns to Request constructor and as readwrite attributes
- intent_type_test.cpp: 6 gtest cases covering defaults, assignment,
  integer values, field independence, copy, and batch usage

Relates to: TENT roadmap "Transfer Intent API"

* ci: retrigger CI

* fix: keep intent type binding scoped

* test: wire intent type coverage into cmake

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Yanshu <237344440@qq.com>
…cache-ai#2814)

* [TENT] admission queue: add deadline proximity promotion

When a queued owner's remaining slack (deadline_ns - now) falls below
the configurable promotion_slack_ns threshold, it is promoted to the
front of the dispatch queue via stable_partition, ahead of owners with
comfortable slack or no deadline.

This complements the existing step-2 EDF ordering by dynamically
boosting urgency as deadlines approach, ensuring near-deadline
transfers get dispatched preferentially even if they were admitted
after requests with later deadlines.

Key design choices:
- Opt-in: promotion_slack_ns defaults to 0 (disabled).
- Requires deadline_aware = true (like all deadline features).
- stable_partition preserves EDF order within promoted/non-promoted
  groups.
- Composes with step-3 drop: promotion happens first, then infeasible
  owners are still dropped from the (now-reordered) front.
- NowProvider reused from step-3 (falls back to steady_clock).

Ref: RFC kvcache-ai#2519 step 4

* fix: wire deadline promotion runtime config

* fix: avoid deque stable partition in dispatch

* perf: reuse deadline promotion scratch buffers

* bench: add deadline promotion hot-path benchmark

* perf: retain faster local partition buffers

* perf: retain benchmarked stable partition

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Yanshu <237344440@qq.com>
…che-ai#2820)

* [transfer-engine] Add show-link diagnostic tool for NIC topology inspection

Add a connectivity diagnostic utility that displays local RDMA NIC
information and topology selection matrix. This helps operators
understand which NICs are discovered, their NUMA affinity, link speed,
and how the topology-aware device selection maps storage types to NICs.

Components:
- show_links.h/cpp: Core logic (collectLocalNics, buildShowLinksReadable/Json)
- show_link.cpp: CLI binary with --json/--discover_only flags
- C API: showLinks() exposed via transfer_engine_c.h
- rdma_context: Add activeWidth() accessor for bandwidth calculation
- show_links_test.cpp: Basic gtest coverage

Tested on 4-node H20 cluster (5 NICs per node, mlx5_0 + mlx5_bond_0..3):
- NIC discovery: 5/5 devices found on both nodes
- NUMA topology: Correctly distinguishes preferred vs available NICs
- Cross-node probe: Verified at 0.5ms latency (3 consecutive runs)
- Speed calculation: 200Gbps (HDR 50Gbps x 4 lanes) matches ibstat

* style: clang-format show_links.cpp and transfer_engine.cpp

* fix: respect show-link json output

* fix: handle show-links before engine init

* fix: make show-link discover-only discover topology

* fix: show topology nics without rdma transport

* fix: harden show-link C API and speed reporting

* style: format show-links JSON assertion

* fix: include integer types in show-links header

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Yanshu <237344440@qq.com>
…#2812)

* [TENT] admission queue: add deadline proximity promotion

When a queued owner's remaining slack (deadline_ns - now) falls below
the configurable promotion_slack_ns threshold, it is promoted to the
front of the dispatch queue via stable_partition, ahead of owners with
comfortable slack or no deadline.

This complements the existing step-2 EDF ordering by dynamically
boosting urgency as deadlines approach, ensuring near-deadline
transfers get dispatched preferentially even if they were admitted
after requests with later deadlines.

Key design choices:
- Opt-in: promotion_slack_ns defaults to 0 (disabled).
- Requires deadline_aware = true (like all deadline features).
- stable_partition preserves EDF order within promoted/non-promoted
  groups.
- Composes with step-3 drop: promotion happens first, then infeasible
  owners are still dropped from the (now-reordered) front.
- NowProvider reused from step-3 (falls back to steady_clock).

Ref: RFC kvcache-ai#2519 step 4

* [transfer-engine] Add graceful shutdown for SIGTERM/SIGINT/SIGABRT

When a Transfer Engine process is killed by SIGTERM/SIGINT/SIGABRT,
RDMA resources (QPs, MRs) are left dangling. The peer side's QP still
references the now-invalid MR, causing RDMA READ to silently return
undefined data instead of an error.

This adds an opt-in enableGracefulShutdown() API that:
- Registers atexit() to call freeEngine() on all active engines
- Installs signal handlers that call exit(128+signo) to trigger atexit
- Ensures the existing deconstruct() path (QP destroy + MR dereg) runs

The API is opt-in to avoid interfering with applications that manage
their own signal handlers (e.g., gRPC, Python interpreters).

* fix: make graceful shutdown signal path safe

* fix: cover tent shutdown and preserve abort semantics

* fix: preserve graceful shutdown across moves

* fix: avoid post-fork shutdown handler hang

---------

Co-authored-by: 彦纾 <wangyanshu.wys@alibaba-inc.com>
Co-authored-by: Yanshu <237344440@qq.com>
…ers (kvcache-ai#2842)

* [TE] Make ThreadLocalStorage per-instance and reclaim per-thread holders

Fixes kvcache-ai#2717.

The previous implementation kept its thread-local slot as a
'thread_local static' member of the class template — one slot per
template instantiation, shared by every ThreadLocalStorage<T> instance
in the process — so two instances (e.g. two engines' SegmentManager
remote-desc caches) aliased each other's per-thread state. The slot was
also a raw pointer to a heap holder that nothing ever deleted, so the
deregistration logic in the holder destructor was unreachable and every
thread leaked one holder per instantiation.

Rework the storage around a per-thread map keyed by a process-monotonic
instance id (never reused, so a recycled allocation cannot alias a
stale entry), with a one-entry cache in front so the common get() stays
one thread_local access plus a compare. Per-thread values are owned by
the thread and destroyed at thread exit. A control block jointly owned
by the storage and every thread node makes both teardown orders safe:
a thread exiting first deregisters from the registry; an owner
destroyed first marks the block dead and exiting threads skip the
registry. forEach() runs under the registry mutex and visits exactly
the live registered values.

Validation (96-core box, GCC 13):
- New thread_local_storage_test: 8/8. Against the previous
  implementation the same binary fails 5/7 (aliasing, deregistration,
  resurrection, forEach, churn) and LeakSanitizer reports 288 bytes in
  18 allocations from get().
- ASAN/UBSAN and TSAN clean for 50 repeated runs each (both
  teardown orders exercised).
- Full TENT suite 23/23 (SegmentManager's tl_remote_cache_ exercises
  the storage end-to-end).
- get() hot path at -O3: 0.33-0.41 ns before, 0.66-0.69 ns after
  (+~0.3 ns; the caller's next step is a clock_gettime and map lookup).

* review: null-check control in sweepDeadNodes

Unreachable today (the sweep runs before try_emplace on the same thread
and control is assigned immediately after emplace by a nothrow
shared_ptr copy), but consistent with ~ThreadNode's defensive check and
robust to future reordering.
Co-authored-by: KMSorSMS <yzwliam@126.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Enable drain jobs to migrate LOCAL_DISK replicas by reusing the existing Move flow with a new LOCAL_DISK transfer path:

- replica.h: get_segment_names() returns transport_endpoint for LOCAL_DISK
- client_service: ExecuteReplicaTransfer adds LOCAL_DISK branch (SSD -> staging buffer -> TransferWrite -> target MEMORY)
- file_storage: expose LoadBatchFromLocalDisk (AllocateBatch + BatchLoad) for client access
- master_service: CreateMoveTask falls back to LOCAL_DISK client lookup; ScheduleDrainJobTasks scans LOCAL_DISK replicas with MEMORY-first dedup; MaybeCompleteDrainJob checks LOCAL_DISK residuals

Reference pattern: ProcessPromotionTasks. No new RPCs, no new master state.
…moves, add e2e tests

Completes the LOCAL_DISK drain migration (Option A) on top of the core
implementation in 60e6d33:

- MoveEnd drops redundant LOCAL_DISK metadata for drain moves only, gated
  by a new is_drain flag threaded through the move task payload
  (ReplicaMovePayload in task_manager.h), the client (Client::Move /
  ExecuteTask), the MasterClient::MoveEnd RPC and the rpc_service /
  master_service handler chain. Manual create_move_task defaults to
  is_drain=false so it no longer discards a live node's LOCAL_DISK backup.
- Scope-narrowing fix for an EDEADLK on segment_mutex_ in MoveEnd (release
  the unique_lock before taking the shared_lock on the same shared_mutex).
- Adds a replica_move_local_disk_transfer_success log in the LOCAL_DISK
  transfer branch of ExecuteReplicaTransfer, making the path observable
  and testable (file_storage.h: signature reformatting only).
- mountLocalDiskSegment / getOffloadRpcAddr test helpers (real_client) and
  an offload_on_evict knob in the in-proc master test config.
- Adds the local_disk_drain_test e2e suite (5 tests): baseline MEMORY-first
  dedup, is_drain gating regression (non-aligned sizes + position-dependent
  pattern), LOCAL_DISK load-failure graceful handling, and eviction-driven
  offload under both offload_on_evict modes.
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.