Skip to content

[Store] L2->L1 promotion-on-hit: observability metrics + max_per_heartbeat knob - #2176

Merged
LujhCoconut merged 4 commits into
kvcache-ai:mainfrom
yzhan1:feature/promotion-metrics-and-kmax-knob
Jun 1, 2026
Merged

LujhCoconut merged 4 commits into
kvcache-ai:mainfrom
yzhan1:feature/promotion-metrics-and-kmax-knob

Conversation

@yzhan1

@yzhan1 yzhan1 commented May 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

Ref #2031

Follow-up to #2071 (L2->L1 promotion-on-hit, merged as da70ac9). Adds Prometheus metrics for the promotion funnel and exposes the previously-hardcoded kMaxPerHeartbeat as a config knob.

Metrics (Prometheus, master_promotion_* prefix)

Funnel — answers "is the feature firing, and where does work get lost?":

Metric Type Description
master_promotion_in_flight gauge Current in-flight tasks
master_promotion_admitted_total counter Tasks past all gates, enqueued
master_promotion_completed_total counter NotifyPromotionSuccess success
master_promotion_completed_bytes_total counter Bytes promoted (sum of source object_size)
master_promotion_failed_total counter NotifyPromotionFailure accepted (holder-reported failure)
master_promotion_cancelled_total counter Task erased because its prerequisite went away (object Remove/Upsert, holder client expired, or NotifyPromotionSuccess could not commit the staged replica)
master_promotion_expired_total counter Reaper sweep — task exceeded put_start_release_timeout_sec

Invariant: admitted = completed + failed + cancelled + expired + in_flight.

failed vs cancelled are intentionally separate so dashboards can distinguish holder-side failures (network/IO/transfer error reported via NotifyPromotionFailure) from upstream-side cancellation (the thing being promoted no longer exists).

Rejection — answers "why aren't we admitting?":

Metric Type Description
master_promotion_rejected_frequency_total counter Below promotion_admission_threshold
master_promotion_rejected_watermark_total counter DRAM at or above eviction high watermark
master_promotion_rejected_cap_total counter promotion_in_flight at promotion_queue_limit

Enough to build a working Grafana panel: rate(admitted) vs rate(completed) shows yield; rate(rejected_*) breaks down where work is dropping; in_flight / promotion_queue_limit shows saturation.

promotion_max_per_heartbeat knob

Was a compile-time constexpr kMaxPerHeartbeat = 1 in PromotionObjectHeartbeat. With the default 10s client heartbeat, this capped per-client throughput at ~6 promotions/min and made promotion_queue_limit = 50000 mostly theoretical.

Exposed as MasterServiceConfig::promotion_max_per_heartbeat (default 1, no behavior change). Wired through:

  • DEFINE_uint32(promotion_max_per_heartbeat, ...) flag in master.cpp
  • default_config.GetUInt32(...) parse from JSON
  • MasterServiceSupervisorConfig plumbing
  • MasterService constructor stores into promotion_max_per_heartbeat_, used in PromotionObjectHeartbeat
  • Constructor clamps 0 → 1 (defense-in-depth — 0 would silently halt promotion delivery)
  • Startup log now includes max_per_heartbeat= alongside threshold= / queue_limit=

Operators with small objects and RDMA-rich clusters can safely raise it; the new MaxPerHeartbeatKnobControlsBatchSize test exercises =3.

Module

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

Type of Change

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

How Has This Been Tested?

promotion_on_hit_test: 33/33 pass (was 28; +5 new), 5 consecutive clean runs.

New tests:

  • MetricsFunnelTracksSuccessfulPromotion — single full lifecycle bumps admitted/completed/completed_bytes correctly and returns in_flight to baseline.
  • MetricsRejectionCountersIncrementOnGateMiss — verifies all three rejection counters (rejected_frequency, rejected_watermark, rejected_cap) increment when their respective branches fire. Watermark sub-case uses eviction_high_watermark_ratio = 0.0 to force the gate.
  • MetricsRemoveMidPromotionCountsAsCancelled — verifies that erasing a key with an in-flight promotion (via Remove) routes the task to promotion_cancelled_total rather than promotion_failed_total.
  • MaxPerHeartbeatKnobControlsBatchSize — sets knob to 3, admits 5 tasks, asserts heartbeats drain as 3 + 2 + 0.
  • MaxPerHeartbeatZeroClampsToOne — pathological =0 config gets clamped to 1 by the constructor; promotion delivery is not silently disabled.

Existing tests enhanced with metric assertions:

  • StalePromotionReaper — asserts promotion_expired increments.
  • RemoveDuringPromotion — asserts promotion_cancelled increments.
  • QueueLimitRejectsBeyondCap — asserts promotion_rejected_cap increments.
  • NotifyFailureReleasesStateImmediately — asserts promotion_failed increments.
  • ClientExpiryClearsPromotionTask — asserts promotion_cancelled increments.

Regression check:

  • file_storage_promotion_test: 11/11 pass
  • master_service_promotion_test_for_snapshot: 5/5 pass
  • offload_on_evict_test: 9/9 pass

scripts/code_format.sh --check --base upstream/main (clang-format-20 in CI image): clean.

Default promotion_max_per_heartbeat = 1 means existing deployments see no behavior change; all metrics zero-emit on cold start so dashboards built against them work immediately.

Checklist

  • I have performed a self-review of my own code.
  • I have formatted my own code using ./scripts/code_format.sh before submitting.
  • I have updated the documentation.
  • I have added tests to prove my changes are effective.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces comprehensive "Promotion-on-hit" metrics and a new configuration parameter, promotion_max_per_heartbeat, to control the number of promotion tasks returned per heartbeat. The changes include the definition, initialization, and integration of various metrics (in-flight, admitted, completed, failed, etc.) within the MasterMetricManager and MasterService. Feedback focuses on ensuring these new metrics are correctly exported for monitoring and maintaining metric invariants across all task cleanup paths, specifically by incrementing terminal counters like promotion_failed when tasks are removed without being successfully committed.

Comment thread mooncake-store/src/master_service.cpp Resolved
Comment thread mooncake-store/include/master_service.h Resolved
Comment thread mooncake-store/src/master_service.cpp Resolved
@yzhan1
yzhan1 marked this pull request as draft May 21, 2026 16:54
@codecov-commenter

codecov-commenter commented May 21, 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 93.01205% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mooncake-store/tests/promotion_on_hit_test.cpp 96.32% 11 Missing ⚠️
mooncake-store/src/master_metric_manager.cpp 87.80% 10 Missing ⚠️
mooncake-store/src/master.cpp 0.00% 5 Missing ⚠️
mooncake-store/include/master_config.h 50.00% 2 Missing ⚠️
mooncake-store/src/master_service.cpp 95.65% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yzhan1
yzhan1 force-pushed the feature/promotion-metrics-and-kmax-knob branch 2 times, most recently from 45ca3c3 to fae0a4d Compare May 23, 2026 08:14
@yzhan1
yzhan1 marked this pull request as ready for review May 23, 2026 08:17
@yzhan1

yzhan1 commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

@ykwd @LujhCoconut PTAL for adding metrics. Thanks

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is a follow-up enhancement to the existing L2→L1 “promotion-on-hit” feature in mooncake-store, adding Prometheus observability metrics for the promotion lifecycle and exposing the previously hardcoded per-heartbeat batching limit as a configurable knob (promotion_max_per_heartbeat).

Changes:

  • Add a set of master_promotion_* Prometheus metrics to track the promotion funnel (in-flight/admitted/completed/bytes/failed/expired/cancelled) and rejection reasons (frequency/watermark/cap).
  • Expose promotion_max_per_heartbeat as a gflag + JSON-configurable field, plumb it through config structures, and apply it in PromotionObjectHeartbeat (with a defensive clamp of 0 -> 1).
  • Extend unit tests to validate key metric increments and validate the new heartbeat batch-size knob behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
mooncake-store/tests/promotion_on_hit_test.cpp Adds assertions for new promotion metrics and adds tests for the max-per-heartbeat batching knob.
mooncake-store/src/master.cpp Introduces --promotion_max_per_heartbeat flag and wires it into config parsing/override logic.
mooncake-store/src/master_service.cpp Implements metric increments across promotion lifecycle + uses promotion_max_per_heartbeat_ to bound heartbeat batches.
mooncake-store/src/master_metric_manager.cpp Defines/serializes new promotion-related Prometheus metrics and includes them in the summary string.
mooncake-store/include/master_service.h Tracks the new promotion_max_per_heartbeat_ field and updates cancellation paths to emit metrics.
mooncake-store/include/master_metric_manager.h Declares promotion metric helpers/getters and adds member fields for the new metrics.
mooncake-store/include/master_config.h Adds promotion_max_per_heartbeat to relevant config structs and propagation helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@LujhCoconut

Copy link
Copy Markdown
Collaborator

Sorry for the late review on your PR.
I have a concern — this looks like a pre-existing issue.

RemoveAll() and BatchRemove() also erase ObjectMetadata entries but don't call ErasePromotionTaskIfPresent. The helper's own docstring says "Call from any path that erases an ObjectMetadata entry", but these two paths were missed. This is a pre-existing bug, but since this PR's scope is promotion-task lifecycle instrumentation, it should be fixed here.

Suggested fix — add one line before each shard->metadata.erase(it):

// master_service.cpp:2440 (RemoveAll):                                                                                
ErasePromotionTaskIfPresent(shard, it->first);
it = shard->metadata.erase(it); 

//  master_service.cpp:2496 (BatchRemove, stale-handle path):                                                           
ErasePromotionTaskIfPresent(shard, key);                                                                            
shard->metadata.erase(it);  

// master_service.cpp:2532 (BatchRemove, normal path):                                                                 
ErasePromotionTaskIfPresent(shard, key);                                                                            
shard->metadata.erase(it); 

Without this, RemoveAll / BatchRemove orphan the promotion_tasks entry, leaking promotion_in_flight_ for up to put_start_release_timeout_sec_ until the reaper sweeps.

Comment thread mooncake-store/src/master_service.cpp Outdated
Comment on lines 607 to 614
if (shard->promotion_tasks.erase(it->first) > 0) {
promotion_in_flight_.fetch_sub(1,
std::memory_order_relaxed);
MasterMetricManager::instance().dec_promotion_in_flight();
MasterMetricManager::instance().inc_promotion_cancelled();
}
it = shard->metadata.erase(it);
} else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Code Smell] this block inlines the same logic as ErasePromotionTaskIfPresent. Consider replacing with the helper call to keep a single source of truth:

ErasePromotionTaskIfPresent(shard, it->first);
it = shard->metadata.erase(it);               

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the comments! PTAL at the fixes

@LujhCoconut

Copy link
Copy Markdown
Collaborator

Thanks for your patience. After addressing the comments above, please resolve the merge conflicts before merging.

@yzhan1
yzhan1 force-pushed the feature/promotion-metrics-and-kmax-knob branch from 306df39 to 8149e35 Compare May 29, 2026 06:57
@yzhan1
yzhan1 requested a review from LujhCoconut May 29, 2026 06:58
@LujhCoconut

LujhCoconut commented May 29, 2026 •

Copy link
Copy Markdown
Collaborator

Thank you for the PR. I've noticed the intermittent CI build failures caused by promotion_on_hit_test.cpp.

Errors while running CTest
The following tests FAILED:
	 18 - promotion_on_hit_test (Failed)
Error: Process completed with exit code 8.

This seems like an easily fixable timing sensitivity。

The StalePromotionReaper test sets put_start_release_timeout_sec=1 and sleeps for 2s waiting for the reaper to expire the task, but the reaper cycle interval is also exactly 1s, and the first cycle is delayed by 10ms due to the strict >
comparison (now - last_discard_time > 1s evaluates to 1s > 1s = false). Under CI load, slight timing drift between task creation and reaper firing can leave the task un-expired when the sleep ends, so the dedup gate still blocks re-enqueue and the pending->size() assertion fails.

A straightforward fix may be like this:

// Set put_start_release_timeout_sec to a value at least 2x smaller than the sleep duration, giving the reaper a comfortable margin:                                                                                                   
                                                                                                                        
config.put_start_release_timeout_sec = 1;                                                                             
// ...                                                                                                                
std::this_thread::sleep_for(std::chrono::seconds(3));  // 3s >> 1s timeout 

Would you be open to fixing this pre-existing timing sensitivity? Either way, it won't block the merge of this PR.

yzhan1 added 2 commits May 30, 2026 04:11
…beat knob

Adds Prometheus metrics for the promotion-on-hit funnel and exposes the
previously-hardcoded kMaxPerHeartbeat as a config knob.

Metrics (master_promotion_*)
----------------------------
Funnel:
  - promotion_in_flight (gauge): mirror of promotion_in_flight_
  - promotion_admitted_total: tasks past all gates, enqueued
  - promotion_completed_total: NotifyPromotionSuccess success
  - promotion_completed_bytes_total: bytes promoted (sum of source object_size)
  - promotion_failed_total: NotifyPromotionFailure accepted
  - promotion_expired_total: reaper Part 4 sweeps

Rejection (per gate):
  - promotion_rejected_frequency_total: below admission threshold
  - promotion_rejected_watermark_total: DRAM at or above high watermark
  - promotion_rejected_cap_total: promotion_in_flight at queue limit

Together: admitted = completed + failed + expired + in_flight. Wires a
working 'promotion feature health' Grafana panel: rate(admitted) vs
rate(completed) shows yield; rate(rejected_*) breaks down where work
is dropping; in_flight / promotion_queue_limit shows saturation.

promotion_max_per_heartbeat knob
--------------------------------
The previous compile-time constant kMaxPerHeartbeat = 1 (in
PromotionObjectHeartbeat) capped per-client throughput at ~6
promotions/min with the default 10s heartbeat, making
promotion_queue_limit = 50000 mostly theoretical.

Exposed as MasterServiceConfig::promotion_max_per_heartbeat, wired
through the flag parser (FLAGS_promotion_max_per_heartbeat) and
default_config.GetUInt32 path. Constructor clamps 0 -> 1 so a
mistyped config doesn't silently halt promotion delivery. Startup
log now includes max_per_heartbeat= alongside threshold= /
queue_limit=.

Default stays 1 (no behavior change on existing deployments).
Operators with small objects + RDMA-rich clusters can raise it; the
new MaxPerHeartbeatKnobControlsBatchSize test exercises 3.

Tests
-----
  - MetricsFunnelTracksSuccessfulPromotion: single full lifecycle
    bumps admitted/completed/completed_bytes correctly and brings
    in_flight back to baseline.
  - MetricsRejectionCountersIncrementOnGateMiss: each of frequency
    and cap counters increments when its branch fires.
  - MaxPerHeartbeatKnobControlsBatchSize: knob=3, 5 admitted tasks
    drain across 3+2+0 heartbeats.
  - MaxPerHeartbeatZeroClampsToOne: pathological config clamps to 1.

Suite: 32/32 pass (was 28; +4 new). No behavior change in existing
tests.
MetricsRejectionCountersIncrementOnGateMiss claimed coverage of all
three rejection counters but only exercised frequency and cap. Add a
sub-case that forces the watermark gate by configuring
eviction_high_watermark_ratio = 0.0, asserting that
promotion_rejected_watermark_total increments.
@yzhan1
yzhan1 force-pushed the feature/promotion-metrics-and-kmax-knob branch from 8149e35 to 4777358 Compare May 30, 2026 04:20
@yzhan1
yzhan1 requested a review from Copilot May 31, 2026 21:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Comment thread mooncake-store/src/master_service.cpp Resolved
Comment thread mooncake-store/src/master_service.cpp Resolved
Comment thread mooncake-store/src/master_service.cpp Resolved
upstream kvcache-ai#2180 introduced EraseMetadataEntry as the centralized
metadata-erase helper and routed RemoveAll, BatchRemove, and
RemoveByRegex through it, so promotion_tasks cleanup on those paths
is already correct on main. The metric instrumentation
(dec_promotion_in_flight + inc_promotion_cancelled) is wired into
EraseMetadataEntry in this branch's earlier commit so every site
that erases metadata bumps the funnel counters consistently.

Add regression tests for the three paths so any future refactor
that reintroduces a metadata.erase without going through
EraseMetadataEntry will fail the suite:
- RemoveAllErasesPromotionTask
- BatchRemoveErasesPromotionTask (normal-completion branch)
- BatchRemoveStaleHandleErasesPromotionTask
Three tests configure put_start_release_timeout_sec=1 and then sleep
2s waiting for the eviction-thread reaper to expire the promotion
task. The reaper schedule is `now - last_discard_time >
put_start_release_timeout_sec_` (strict greater-than), so a 1s
release with a 2s sleep leaves only ~1s margin between the reaper
firing and the assertion. CI runs observed intermittent failures
when scheduling jitter erased that margin.

Bump the sleep to 3s in the three affected tests so the margin is
~2s. Configuration values unchanged.

Affected:
- StalePromotionReaper
- RemoveDuringPromotion
- AllocStartRejectsReapedTask

Verified 5 consecutive clean runs of all three under -j1 build.
@yzhan1
yzhan1 force-pushed the feature/promotion-metrics-and-kmax-knob branch from a9df75b to 0633b29 Compare May 31, 2026 22:21
@yzhan1

yzhan1 commented May 31, 2026

Copy link
Copy Markdown
Contributor Author

@LujhCoconut PTAL. Should've addressed all your comments

@LujhCoconut LujhCoconut left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Thanks for this PR.

@LujhCoconut
LujhCoconut merged commit 579d42d into kvcache-ai:main Jun 1, 2026
17 of 20 checks passed
A-Liuhao pushed a commit to A-Liuhao/Mooncake that referenced this pull request Jun 25, 2026
…tbeat knob (kvcache-ai#2176)

* [Store] L2->L1 promotion-on-hit: Tier A observability + max_per_heartbeat knob

Adds Prometheus metrics for the promotion-on-hit funnel and exposes the
previously-hardcoded kMaxPerHeartbeat as a config knob.

Metrics (master_promotion_*)
----------------------------
Funnel:
  - promotion_in_flight (gauge): mirror of promotion_in_flight_
  - promotion_admitted_total: tasks past all gates, enqueued
  - promotion_completed_total: NotifyPromotionSuccess success
  - promotion_completed_bytes_total: bytes promoted (sum of source object_size)
  - promotion_failed_total: NotifyPromotionFailure accepted
  - promotion_expired_total: reaper Part 4 sweeps

Rejection (per gate):
  - promotion_rejected_frequency_total: below admission threshold
  - promotion_rejected_watermark_total: DRAM at or above high watermark
  - promotion_rejected_cap_total: promotion_in_flight at queue limit

Together: admitted = completed + failed + expired + in_flight. Wires a
working 'promotion feature health' Grafana panel: rate(admitted) vs
rate(completed) shows yield; rate(rejected_*) breaks down where work
is dropping; in_flight / promotion_queue_limit shows saturation.

promotion_max_per_heartbeat knob
--------------------------------
The previous compile-time constant kMaxPerHeartbeat = 1 (in
PromotionObjectHeartbeat) capped per-client throughput at ~6
promotions/min with the default 10s heartbeat, making
promotion_queue_limit = 50000 mostly theoretical.

Exposed as MasterServiceConfig::promotion_max_per_heartbeat, wired
through the flag parser (FLAGS_promotion_max_per_heartbeat) and
default_config.GetUInt32 path. Constructor clamps 0 -> 1 so a
mistyped config doesn't silently halt promotion delivery. Startup
log now includes max_per_heartbeat= alongside threshold= /
queue_limit=.

Default stays 1 (no behavior change on existing deployments).
Operators with small objects + RDMA-rich clusters can raise it; the
new MaxPerHeartbeatKnobControlsBatchSize test exercises 3.

Tests
-----
  - MetricsFunnelTracksSuccessfulPromotion: single full lifecycle
    bumps admitted/completed/completed_bytes correctly and brings
    in_flight back to baseline.
  - MetricsRejectionCountersIncrementOnGateMiss: each of frequency
    and cap counters increments when its branch fires.
  - MaxPerHeartbeatKnobControlsBatchSize: knob=3, 5 admitted tasks
    drain across 3+2+0 heartbeats.
  - MaxPerHeartbeatZeroClampsToOne: pathological config clamps to 1.

Suite: 32/32 pass (was 28; +4 new). No behavior change in existing
tests.

* [Store] L2->L1 promotion-on-hit: cover watermark gate in rejection test

MetricsRejectionCountersIncrementOnGateMiss claimed coverage of all
three rejection counters but only exercised frequency and cap. Add a
sub-case that forces the watermark gate by configuring
eviction_high_watermark_ratio = 0.0, asserting that
promotion_rejected_watermark_total increments.

* [Store] L2->L1 promotion-on-hit: cover RemoveAll/BatchRemove cleanup

upstream kvcache-ai#2180 introduced EraseMetadataEntry as the centralized
metadata-erase helper and routed RemoveAll, BatchRemove, and
RemoveByRegex through it, so promotion_tasks cleanup on those paths
is already correct on main. The metric instrumentation
(dec_promotion_in_flight + inc_promotion_cancelled) is wired into
EraseMetadataEntry in this branch's earlier commit so every site
that erases metadata bumps the funnel counters consistently.

Add regression tests for the three paths so any future refactor
that reintroduces a metadata.erase without going through
EraseMetadataEntry will fail the suite:
- RemoveAllErasesPromotionTask
- BatchRemoveErasesPromotionTask (normal-completion branch)
- BatchRemoveStaleHandleErasesPromotionTask

* [Store] L2->L1 promotion-on-hit: bump reaper-test sleep margin to 3s

Three tests configure put_start_release_timeout_sec=1 and then sleep
2s waiting for the eviction-thread reaper to expire the promotion
task. The reaper schedule is `now - last_discard_time >
put_start_release_timeout_sec_` (strict greater-than), so a 1s
release with a 2s sleep leaves only ~1s margin between the reaper
firing and the assertion. CI runs observed intermittent failures
when scheduling jitter erased that margin.

Bump the sleep to 3s in the three affected tests so the margin is
~2s. Configuration values unchanged.

Affected:
- StalePromotionReaper
- RemoveDuringPromotion
- AllocStartRejectsReapedTask

Verified 5 consecutive clean runs of all three under -j1 build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants