[Store] Support LOCAL_DISK replica migration during drain (Option A)#31
Closed
liangxu2000 wants to merge 57 commits into
Closed
[Store] Support LOCAL_DISK replica migration during drain (Option A)#31liangxu2000 wants to merge 57 commits into
liangxu2000 wants to merge 57 commits into
Conversation
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>
…e-ai#2417) Co-authored-by: fatSheep <tzh2005t@163.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>
Signed-off-by: Schatten <czhengt@qq.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>
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_DISKreplicas (client-side SSD offload files written byFileStorage) were silently skipped becauseReplica::get_segment_names()returned an empty vector for the
LOCAL_DISKtype. Any data that had beenevicted 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
DRAINEDwhileLOCAL_DISKresiduals exist. This PR (Step 2) adds theactual 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_DISKreplica is just a file on the source client'slocal SSD. To migrate it, we need to (a) read that file into a memory buffer,
then (b) RDMA-write the buffer into a
MEMORYsegment on the target node.Mooncake already has battle-tested primitives for both steps:
BatchLoad(O_DIRECT file → aligned memory) andTransferWrite(registeredmemory → remote segment). The promotion path (
ProcessPromotionTasks) usesexactly this pattern within a single node; we simply redirect the
TransferWritedestination to a remote target allocated by the drainscheduler.
Why reuse the Move flow instead of a new mechanism?
The existing drain
infrastructure (
ScheduleDrainJobTasks→CreateMoveTask→ client executestransfer →
MoveEndfinalizes metadata) already provides:kMaxDrainUnitRetriesmax_concurrency)MaybeCompleteDrainJob)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):
Implementation steps:
Visibility.
Replica::get_segment_names()now returns thetransport_endpointforLOCAL_DISKreplicas. Previously it returned anempty vector, making them invisible to the scheduler.
Scheduling with MEMORY-first dedup.
ScheduleDrainJobTasksscans allreplicas on the draining segment. If a key has both a
MEMORYand aLOCAL_DISKreplica on the same client, only theMEMORYreplica isscheduled for migration (it is already in RAM — cheaper to transfer). The
redundant
LOCAL_DISKmetadata is cleaned up inMoveEnd(see step 5).Only keys that are
LOCAL_DISK-only (memory was already evicted) gothrough the file-transfer path.
File transfer branch.
ExecuteReplicaTransfergains aLOCAL_DISKsource branch:
LoadBatchFromLocalDiskcallsAllocateBatch(allocates anO_DIRECT-aligned staging buffer in registered memory) +
BatchLoad(reads the SSD file via
align_down/align_upoffset correction).TransferWriteRDMA-writes the staging buffer into the target's freshlyallocated
MEMORYreplica.replica_move_local_disk_transfer_successlog is emitted on success,making the path observable and testable.
is_drainflag (see Design notes). A boolean is threaded from thescheduler through
ReplicaMovePayload→Client::Move→MoveEndRPC.Conditional metadata cleanup. When a drain move relocates a
MEMORYreplica, the same key's
LOCAL_DISKreplica on the source client is nowredundant (it is a disk backup of the memory data we just moved, and the
target already holds that data).
MoveEnddrops theLOCAL_DISKmastermetadata only when
is_drain=true. The physical SSD file isintentionally left on disk as a safety net (no destructive deletion);
physical cleanup is a separate future step.
Completion guard.
MaybeCompleteDrainJobchecks forLOCAL_DISKresiduals before marking a segment
DRAINED. If anyLOCAL_DISK-onlykeys remain (e.g. because they failed), the segment stays in
DRAININGand 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_DISKtransfer path(
LoadBatchFromLocalDisk→ staging buffer →TransferWrite) is exercised.Module
mooncake-transfer-engine)mooncake-store)mooncake-ep)mooncake-pg)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-common)mooncake-rl)Type of Change
Changes
include/replica.hget_segment_names()returns thetransport_endpointforLOCAL_DISKso the drain scheduler can locate them.src/client_service.cppExecuteReplicaTransfer: newLOCAL_DISKsource branch (SSD -> staging buffer viaLoadBatchFromLocalDisk->TransferWrite-> target MEMORY). Adds a..._local_disk_transfer_successlog so the path is observable/testable.src/file_storage.cppLoadBatchFromLocalDisk(AllocateBatch+BatchLoad) exposed for the client.src/master_service.cppCreateMoveTask: client fallback forLOCAL_DISKsources, plus anis_drainflag (defaultfalse; drain sets ittrue).ScheduleDrainJobTasks: scansLOCAL_DISKreplicas with MEMORY-first dedup.MaybeCompleteDrainJob: checksLOCAL_DISKresidue before marking a segmentDRAINED.MoveEnd: only for drain moves (is_drain=true) drops the redundantLOCAL_DISKmetadata after aMEMORYmove, with a scope-narrowing fix for anEDEADLKonsegment_mutex_(unique -> shared on the samestd::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.cppis_drainflag from the drain scheduler through the move task payload (ReplicaMovePayload), the client (Client::Move/ExecuteTask), and theMoveEndRPC chain, soMoveEndcan distinguish drain moves from the publiccreate_move_taskAPI. Manual moves default tois_drain=false.include/real_client.h/src/real_client.cppmountLocalDiskSegment()+getOffloadRpcAddr()(used by the e2e test to register aLOCAL_DISKsegment).include/master_config.h/tests/test_server_helpers.hInProcMasterConfig/ builder gainoffload_on_evict, passed through toWrappedMasterServiceConfig(test infra).tests/e2e/CMakeLists.txtlocal_disk_drain_test.tests/e2e/local_disk_drain_test.cppDesign notes
is_drainflag — distinguishing drain moves from manual moves. Thepublic
create_move_taskAPI (used for load-balancing) and the internaldrain scheduler both go through the same
Move→MoveEndcode path.Without a distinguishing flag,
MoveEndwould unconditionally drop thesource client's
LOCAL_DISKmetadata after any MEMORY move — silentlydiscarding a durable SSD backup on a still-live node during a manual move.
To prevent this, a boolean
is_drainis threaded through:ScheduleDrainJobTaskssets ittrueinReplicaMovePayload→task_manager.hcarries it in the task →Client::Move/ExecuteTaskforwards it →
MasterClient::MoveEndpasses it in the RPC →MasterService::MoveEndgates theLOCAL_DISKmetadata removal onis_drain == true. Manual moves default tois_drain=false, preservingthe source's
LOCAL_DISKreplica.MEMORYand a
LOCAL_DISKreplica on the draining client, only theMEMORYreplicais migrated; the
LOCAL_DISKmetadata is dropped inMoveEnd(the SSD fileis intentionally retained on disk as a safety net — physical cleanup is
future work).
get_global_mem_used_ratio()isallocated / capacitysummed across all mounted segments. Mounting alarge 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 withenable_offload). It is gatedby
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 keyalready has a
LOCAL_DISKreplica by the time eviction runs.offload_on_evict=true: PutEnd does not offload; a key is queued foroffload only when eviction picks it and finds no
LOCAL_DISKreplica 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:
Test scenarios (5 tests, all pass):
LocalDiskDrainTest.DrainLocalDiskReplicasToPeerPurpose: 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.
LocalDiskDrainTest.ManualMovePreservesLocalDiskBackupPurpose: regression guard for the
is_draingating — prove that amanual move (
is_drain=false) does NOT drop the source's LOCAL_DISKreplicas, 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_taskAPI, then drain client1 → client3. Keys use a mixof 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_successcount),byte-for-byte correct on client3 (guards the O_DIRECT aligned-read offset
fix in
BatchLoad). If the manual move had wrongly dropped LOCAL_DISKmetadata,
succeeded_unitswould be 0.LocalDiskDrainTest.DrainFailsGracefullyWhenLocalDiskDataMissingPurpose: 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_failederror branch hit,source segment NOT marked DRAINED, client2's MEMORY copy byte-for-byte
intact. Exercises the retry / terminal-failure accounting
(
kMaxDrainUnitRetries = 3).LocalDiskDrainEvictTest.DrainAfterEviction_OffloadOnEvictPurpose: verify drain works end-to-end in a realistic eviction scenario
with
offload_on_evict=true(two-step eviction), where some keys areMEMORY-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.
LocalDiskDrainEvictTest.DrainAfterEviction_ImmediateOffloadPurpose: same as above but with
offload_on_evict=false(offload atPutEnd), 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:
local_disk_drain_test, 5/5 PASSED)Log evidence (what you see when running the tests):
The tests emit structured glog output (
GLOG_logtostderr=1) and use aCapturingLogSinkto programmatically assert that specific code paths weretaken. Here is what the key evidence looks like:
Test 2 — LOCAL_DISK transfer path exercised (success):
This proves the full SSD → staging buffer →
TransferWrite→ target MEMORYpath ran for every key, including non-aligned sizes (7001, 12000, 5000 bytes)
that exercise the O_DIRECT
align_down/align_upoffset correction inBatchLoad. The test asserts this count >= 4.Test 3 — graceful failure when SSD files are deleted (4 keys × 3 retries):
Each key fails exactly 3 times (
kMaxDrainUnitRetries = 3), producing 12local_disk_load_failedlines total. The test asserts:status_name == "FAILED"(job reached terminal state, no hang)succeeded_units == 0(nothing was migrated)DRAINEDdid not corrupt or drop the live replica)
Tests 4/5 — eviction-driven drain (final job query):
All 16 keys (8 via MEMORY path + 8 via LOCAL_DISK path) migrated successfully;
migrated_bytes = 16 × 4 MB = 64 MiBconfirms no data was silently dropped.Checklist
./scripts/code_format.shpre-commit run --all-filesand all hooks passAI Assistance Disclosure
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.