Skip to content

[Store] Add Fast-Fail for Impossible OffsetAllocator Requests - #3222

Open
bitborne wants to merge 3 commits into
kvcache-ai:mainfrom
bitborne:perf/offset-allocator-fast-fail
Open

[Store] Add Fast-Fail for Impossible OffsetAllocator Requests#3222
bitborne wants to merge 3 commits into
kvcache-ai:mainfrom
bitborne:perf/offset-allocator-fast-fail

Conversation

@bitborne

@bitborne bitborne commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

Cache a conservative upper-bound hint for the largest allocatable free region in OffsetAllocator and use it to reject impossible requests before acquiring the allocator mutex.

The main changes are:

  • Initialize the atomic hint from allocator state during construction and deserialization.
  • Check the requested size against the hint before entering the mutex-protected allocation path.
  • Keep successful allocations off the hint-update path. Allocation can only reduce the largest free region, so the previous value remains a safe upper bound.
  • Tighten the hint after a request passes the hint but fails in the mutex-protected allocator.
  • Refresh the hint after deallocation because freeing and neighbor merging can increase the largest allocatable region.
  • Keep the serialized format unchanged because the hint is derived from allocator state.
  • Add a reusable multithreaded benchmark covering direct OffsetAllocator calls and the same-segment Put allocation stage, including capacity pressure, deterministic fragmentation, configurable mixed success/failure ratios, and a successful allocate/free control.

The mutex-protected allocator remains authoritative. A stale larger hint can only allow a request to fall through to the existing locked path; it cannot make an invalid allocation succeed. The first locked failure tightens the hint for later requests. memory_order_relaxed is sufficient because the atomic value is only a filtering hint and does not publish or protect allocator metadata.

This change is complementary to #2445 and #2797, which use the largest free region to rank segments but do not modify OffsetAllocator internals. It also avoids replacing the current mutex with a shared mutex, as discussed in the closed PR #2695.

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

How Has This Been Tested?

Test commands:

cmake --build build \
  --target offset_allocator_test offset_allocator_concurrency_bench \
  -j8

./build/mooncake-store/tests/offset_allocator_test

taskset -c 0-15 \
  ./build/mooncake-store/benchmarks/offset_allocator_concurrency_bench \
  --threads=16 \
  --iterations=1000000 \
  --warmup_iterations=10000 \
  --layer=offset_allocator \
  --scenario=mixed \
  --mixed_failure_ratio=0.30

The A/B benchmark used RelWithDebInfo builds on the same host, pinned to CPUs 0-15. Throughput and per-operation latency were measured in separate phases. Each long-running result below contains 16 million measured operations.

The high-water workloads use a 1 GiB allocator at 93.75% actual utilization, with a 64 MiB largest free region. Successful operations allocate and immediately release 4 KiB. Expected-failure operations request 128 MiB and therefore cannot fit. For example, a 30% mixed workload contains approximately 30 expected failures and 70 successful allocate/free operations per 100 attempts.

First, the lazy upper-bound policy was compared with the eager policy that refreshed the atomic value after every successful allocation. The binaries used the same benchmark and differed only in the hint-update policy.

Scenario Eager-refresh QPS Lazy-hint QPS QPS change Eager p99 Lazy p99
Successful allocate/free 838.54 K 920.17 K +9.7% 82,548 ns 82,908 ns
5% expected failures 790.06 K 960.61 K +21.6% 81,826 ns 84,151 ns
30% expected failures 1.13 M 1.16 M +2.7% 80,053 ns 78,540 ns
100% expected failures 1.95 B 2.33 B +19.2% 80 ns 70 ns

This comparison shows that avoiding the successful-allocation refresh recovers a substantial part of the eager policy's success-path cost while preserving the fast-fail behavior.

The final lazy-hint implementation was also compared with the upstream allocator, which has no atomic pre-check:

Scenario Upstream QPS Lazy-hint QPS QPS change Upstream p99 Lazy p99
Successful allocate/free 1.03 M 932.93 K -9.1% 82,626 ns 82,515 ns
5% expected failures 1.01 M 880.45 K -13.1% 78,939 ns 85,181 ns
30% expected failures 1.15 M 1.18 M +3.1% 69,932 ns 79,429 ns
100% expected failures 10.73 M 2.34 B +217x 25,238 ns 71 ns

The mixed results make the trade-off explicit: the synthetic workload crosses into positive aggregate throughput near a 30% impossible-request ratio. The successful control intentionally frees every allocation, so it exercises the remaining free-time refresh on every operation; append/write-once workloads are expected to refresh less frequently.

The benchmark also covers the same-segment Put allocation stage and deterministic fragmentation. All benchmark runs completed with zero unexpected results in both the throughput and latency phases.

User-space lock profiling was performed on the eager-refresh fast-fail variant using a pthread_mutex_lock uprobe and the syscalls:sys_enter_futex tracepoint. Across 3,232,000 measured and warmup allocation attempts, pthread_mutex_lock calls decreased from 3,232,057 to 57 and futex syscalls decreased from 1,197,395 to 22. The final lazy policy retains the same repeated-request fast path after the first mutex-protected failure tightens the hint. QPS measured while perf probes were active was not used because probe overhead affects the two implementations asymmetrically.

Test results:

  • Unit tests pass
  • Integration tests pass (if applicable)
  • Manual testing done (focused 16-thread A/B benchmark described above)

The complete OffsetAllocatorTest suite passed: 46/46 tests. The tests cover hint tightening after failure, refresh after free, serialization/deserialization, SmallFloat bin boundaries, multiplier values 1 through 4, and mutex-free rejection after the hint has been tightened.

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 on all modified files and all applicable hooks pass
  • I have updated the documentation (if applicable)
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: N/A; the production implementation is below 500 LOC excluding tests and benchmark code and is not a major architectural change

AI Assistance Disclosure

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

Tool: OpenAI Codex

Used for code navigation, implementing the allocator changes, drafting unit tests and benchmark scenarios, and reviewing implementation details and benchmark results. The submitter reviewed every changed line and is responsible for understanding and defending the changes.

bitborne added 2 commits July 30, 2026 16:13
Signed-off-by: Schatten <czhengt@qq.com>
Signed-off-by: Schatten <czhengt@qq.com>
@codecov-commenter

codecov-commenter commented Jul 31, 2026

Copy link
Copy Markdown

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

Codecov Report

❌ Patch coverage is 31.65266% with 244 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../benchmarks/offset_allocator_concurrency_bench.cpp 0.00% 239 Missing ⚠️
mooncake-store/tests/offset_allocator_test.cpp 95.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

Signed-off-by: Schatten <czhengt@qq.com>
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.

2 participants