Skip to content

[CI/Build] Gate CPU and T-One RDMA performance regressions - #4639

Open
stmatengss wants to merge 9 commits into
mainfrom
sima/ci-performance-gate
Open

stmatengss wants to merge 9 commits into
mainfrom
sima/ci-performance-gate

Conversation

@stmatengss

@stmatengss stmatengss commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Add CI performance gates that compare a baseline and candidate, show per-case medians, regression percentages, thresholds and noise in the Actions Summary, and fail when a threshold is exceeded. The CPU suite covers the existing allocator benchmark. The opt-in RDMA suite measures TENT DRAM read/write on a fixed T-One host pair.

For RDMA, the GitHub controller submits exact baseline/candidate/harness SHAs, downloads a JSON result envelope, checks its provenance, and independently enforces the policy. The installable mooncake-rdma-perf tone-cli adapter leases both hosts, builds both revisions with the same benchmark source and provisioned RDMA libraries, runs an integrity smoke and five alternating pairs, and preserves raw logs in T-One. A small --dram_numa_node benchmark option fixes DRAM placement for this suite. Bandwidth loss above 5% or P99 growth above 10% fails; incomplete, invalid, unstable, stale, or failed executions also fail.

Both suites freeze the baseline's tracked CMake recipes as well as the benchmark source, including target definitions, parent flags and included modules; candidate-only recipes are removed from disposable checkouts. The build manifest is preserved and RDMA results must carry matching configuration hashes. This compares implementation changes under a fixed build configuration; build-system changes require separate validation.

RDMA targets run directly in their own process groups so normal shutdown delivers one SIGTERM; the worker watchdog and forced cleanup still bound their lifetime. The shared GitHub concurrency group uses queue: max to retain up to 100 pending runs without replacing earlier waiting runs.

Collection stops at the first failed, timed-out or malformed sample and preserves partial results. A 15-minute CPU collection budget, separate setup/build time limits, scoped process-group cleanup and the T-One stop API leave time for comparison/artifact publication. Invalid policy containers and malformed returned errors produce an ERROR report rather than an uncaught exception. Existing SGLang/vLLM T-One jobs are unchanged.

CI prerequisite: update the test DSL's lease-free BatchProbeKey call to pass the default GrantLeasePolicy, matching the current main API. The stale call was blocking CTest and four TENT build configurations.

Deployment boundary: the adapter and example host configuration are included, but no live T-One template or host configuration has been changed. Automatic RDMA execution is off until RDMA_PERF_ENABLED=true; the template, SSH/NIC/GID/NUMA configuration and HTTPS result-download origin must be commissioned first. Real-hardware A/A calibration is required before choosing production thresholds or making the check required. Fork PRs lack secrets and fail closed when enabled; a reviewed manual run does not automatically become a required check on the fork PR.

Design/RFC: #4734. Overlap searches found transport/benchmark work but no existing T-One RDMA regression gate. This reuses native tebench and extends the existing CPU gate rather than adding a competing benchmark implementation.

Suggested Merge Priority

  • P2 — Normal: New CI feature for a confirmed use case

Module

  • Transfer Engine (mooncake-transfer-engine)
  • Mooncake Store (mooncake-store) — test helper compatibility only
  • CI/CD
  • Docs

Type of Change

  • New feature
  • Documentation update

How Has This Been Tested?

Test commands:

python3 -m unittest discover -s scripts/ci/tests -p 'test_*perf*.py' -v
actionlint -shellcheck='' -ignore '^unexpected key "queue" for "concurrency" section\.' \
  .github/workflows/performance.yml .github/workflows/rdma-performance.yml
bash -n scripts/ci/performance/tone/mooncake-rdma-perf/install.sh \
  scripts/ci/performance/tone/mooncake-rdma-perf/run.sh
pre-commit run --files $(git diff --name-only --diff-filter=ACMR origin/main...HEAD)
git diff --check

Test results:

  • Unit tests pass — all 39 performance tests pass locally at head a9cea2c5 (14.7 seconds), including the native CMake/C++ workload-freezing fixture, policy/error reporting, fail-fast collection and budget limits, RDMA provenance/control fixtures, descendant cleanup, duplicate-SIGTERM detection, and rejection of dead or unresponsive targets. A separate process-fixture reproduction with real GNU timeout failed 20/20 times with the original target wrapper and 0/20 with the fix; this is control-path evidence, not a native RDMA measurement. CodeRabbit reviewed the four changed files and raised 0 issues.
  • Integration tests pass — live T-One deployment, actual RDMA connectivity, hardware A/A calibration, and end-to-end cancellation on the deployed service remain pending. Fixture tests do not establish hardware performance. Hosted Linux, macOS and performance checks are queued/running against head a9cea2c5; RDMA execution is disabled and its workflow is skipped.
  • Manual testing done — PR-scoped pre-commit, shell syntax and diff checks pass. Actionlint 1.7.12 passes with its unsupported queue key diagnostic explicitly excluded and shellcheck disabled (the unchanged CPU workflow has an SC2129 style diagnostic). GitHub accepted the RDMA workflow at this head. queue: max is documented in GitHub concurrency controls.

Checklist

  • I have performed a self-review of my own code — agent review completed; human line-by-line review is pending
  • I have formatted my code using ./scripts/code_format.sh via the changed-line pre-commit hook
  • I have run pre-commit on the files changed in this PR and all hooks pass
  • I have updated the documentation
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: I have filed an RFC issue — [RFC]: Add an opt-in T-One RDMA performance regression gate #4734

AI Assistance Disclosure

  • No AI tools were used
  • AI tools were used

Codex implemented the workflows, collectors/comparator, T-One adapter, NUMA benchmark flag, frozen build recipes, failure handling, test helper compatibility fix, tests and documentation, and ran the recorded local checks. The human submitter still needs to review every changed line and defend the change end-to-end. Real T-One/RDMA commissioning is explicitly pending; no new hardware performance results are claimed.

@github-actions github-actions Bot added documentation Improvements or additions to documentation run-ci CI/Build labels Oct 13, 2026
@stmatengss
stmatengss marked this pull request as ready for review October 13, 2026 18:05
Copilot AI balanced review requested due to automatic review settings October 13, 2026 18:05

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.

🟡 Changes recommended

Timeout handling can prevent artifact publication, and workload freezing and malformed-policy handling are incomplete.

3 open findings
What changed in this PR

Adds a CI performance gate for allocator CPU regressions.

Changes:

  • Compares paired baseline/candidate benchmark measurements.
  • Publishes summaries, diagnostics, and artifacts.
  • Adds policy, tests, workflow, and contributor documentation.
File Description
.github/​workflows/​performance.yml Builds revisions and runs the gate.
CONTRIBUTING.md Links the performance-gate documentation.
scripts/​ci/​performance/​README.md Documents behavior and calibration.
scripts/​ci/​performance/​perf_gate.py Collects, validates, and compares results.
scripts/​ci/​performance/​policy.json Defines workloads and thresholds.
scripts/​ci/​tests/​test_perf_gate.py Tests collection and comparison behavior.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/performance.yml
Comment thread scripts/ci/performance/perf_gate.py Outdated
Comment thread scripts/ci/performance/perf_gate.py
@stmatengss stmatengss changed the title [CI/Build] Fail CPU performance regressions with CI summaries [CI/Build] Gate CPU and T-One RDMA performance regressions Oct 17, 2026
Comment thread scripts/ci/tests/test_rdma_perf.py Fixed
Comment thread scripts/ci/performance/rdma_worker.py Fixed
Comment thread scripts/ci/performance/rdma_worker.py Fixed
Comment thread scripts/ci/performance/rdma_worker.py Fixed
@stmatengss

Copy link
Copy Markdown
Collaborator Author

@ykwd PTAL

@stmatengss

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

# Conflicts:
#	mooncake-transfer-engine/benchmark/utils.cpp

Co-authored-by: stmatengss <11641725+stmatengss@users.noreply.github.com>

Copilot AI commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merged main and resolved the conflict in mooncake-transfer-engine/benchmark/utils.cpp in merge commit e71d261.

@xiaodouzi666

Copy link
Copy Markdown
Contributor

Hi, I got few questions regarding this PR:
The first one is, the signal forwarding of killpg(SIGTERM) and timeout here will cause tebench to receive SIGTERM repeatedly, triggering the secondary signal exit(1) of the TENT handler, causing the sample to be judged as ERROR during normal termination. In my reproduction, there were 184/200 failures. Would removing the timeout outside the target and reusing the worker watchdog be a better solution?

Second is, would it be better to add a queue under the current concurrency configurations? eg., maximum 100 parallel waiting tasks, and I think cancel-in-progress: false will retain only one pending run. The new run will cancel the previously waiting run. Therefore, when multiple PRs share rdma-performance-pair, they will cancel each other's checks.

@Aionw Aionw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the quick follow-up in a9cea2c. Dropping the inner timeout fixes the double SIGTERM on target shutdown, and queue: max fixes the pending-run replacement on the RDMA pair. Both look good to me.

I re-ran the harness tests on the current head (39 pass). Inline comments are below. The one I'd like resolved before this becomes a check on every PR is the frozen CMake recipes (freeze_build.py). The others are workflow hygiene or questions.

for name, path in destinations.items():
if name not in sources:
path.unlink(missing_ok=True)
for name, content in sources.items():

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.

Freezing every tracked CMakeLists.txt from the baseline breaks the CPU gate for any PR that adds, removes or renames a source file under mooncake-store/src. offset_allocator_concurrency_bench links the whole mooncake_store, whose sources are listed explicitly in MOONCAKE_STORE_*_SOURCES. A newly added .cpp is never compiled, so the link fails with undefined symbols. A deleted one fails at configure time with "Cannot find source file".

The README does say an incompatible build layout fails explicitly, but this case is common: 21 of the 234 commits on main in the last month added or removed a .cpp under mooncake-store/src or mooncake-common. Every one of those would have got a red ERROR here.

Could we narrow what gets frozen? For example, keep the benchmark source and mooncake-store/benchmarks/CMakeLists.txt frozen but use the candidate's library recipes. Or, if the candidate fails to build under the frozen recipes, report a neutral "not comparable" result instead of ERROR. (The RDMA gate is mostly unaffected because the TE recipes use file(GLOB ...).)

@@ -0,0 +1,151 @@
name: Performance regression

on:

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.

There's no paths filter, so docs-only or Python-only PRs also pay for two Release builds plus up to 15 minutes of benchmarking. Hosted-runner noise can then turn into an ERROR on a change that cannot affect the allocator. Could we limit this to mooncake-store/**, mooncake-common/**, top-level CMake and scripts/ci/performance/**? If the check is meant to become required, a small no-op job for skipped paths keeps it reporting.

performance:
name: Performance Gate
needs: tests
if: ${{ always() }}

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.

always() also runs after the run is cancelled. With cancel-in-progress: true, two quick pushes leave the superseded run still taking a runner for this job (which matters on PERF_RUNNER), failing "Require harness tests" and publishing an ERROR summary. if: ${{ !cancelled() }} keeps the "run even if tests failed" behavior without that. The same applies to the two always() steps below it.

parser.add_argument(
"--result-origin", default=os.environ.get("TONE_RESULT_ORIGIN", "")
)
parser.add_argument("--timeout", type=int, default=2700)

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.

Question about the time budget. This 2700s deadline starts at job submission, so it includes T-One queueing. The remote worker's 2700s watchdog starts at SSH launch. The worker has to fit a clone, two tebench builds at -j4 (each self.command also has a 900s default), and 48 tebench runs (40 measured plus 8 verify). The measured runs alone take about 10–13 minutes. Do you have the wall-clock time of a full run on the target pair? If the builds take more than about 10 minutes each, routine runs will hit the deadline with nothing wrong in the code.

for key, value in sample.items():
values[key]["samples"].append(value)
finally:
remotes[1].request("stop")

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.

Minor: if stop raises here, it replaces the exception from the sample, and only the stop error lands in errors[]. Since a9cea2c this mostly happens when the target actually died, so it's usually the more useful message anyway. Still, it may be worth catching it and recording both errors.



def environment(cpu_count):
cpus = sorted(os.sched_getaffinity(0))[:cpu_count]

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.

Nit: taking the first cpu_count CPUs from the affinity mask often gives two SMT siblings of the same core on hosted 4-vCPU VMs. The baseline and candidate share the pinning, so the comparison stays fair, but the -2t cases would measure SMT contention and show more spread. Picking CPUs from distinct cores (via topology/thread_siblings_list) would make the 2-thread cases more meaningful. Fine to leave as a follow-up.

@github-actions github-actions Bot added the size/L 1001+ changed lines (additions + deletions) label Oct 27, 2026
ykwd pushed a commit that referenced this pull request Oct 28, 2026
OffsetAllocationHandle stores only a std::weak_ptr<OffsetAllocator>,
but OffsetAllocator::allocate() and createHandleAtNode() construct it
from shared_from_this(). Every successful allocation therefore promotes
the allocator's internal weak reference to a shared_ptr (a
compare-and-swap on the use count), converts that temporary into the
stored weak_ptr (a weak-count increment) and destroys the temporary (a
use-count decrement).

Take a std::weak_ptr in the constructor and pass weak_from_this(), so
an allocation performs only the weak-count increment. The handle still
does not keep the allocator alive, and its destructor still locks the
weak_ptr before freeing.

The two calls differ only for an allocator that no shared_ptr owns:
shared_from_this() throws std::bad_weak_ptr, while weak_from_this()
returns an empty weak_ptr and the handle would never free. That cannot
happen here, because OffsetAllocator's constructors are private and
both create() and deserialize_from() wrap the new object in a
shared_ptr. Existing tests fail if allocate() hands out an empty
weak_ptr, and the new CreateHandleAtNodeAdoptsRestoredNode test fails
if createHandleAtNode(), which restart recovery uses, does.

In offset_allocator_concurrency_bench (the benchmark behind the CPU
performance gate in #4639), successful_allocate_free of 4 KiB rose in
median qps by 16.7% and 16.8% with one thread, by 5.9% to 11.2% with
two (four runs) and by 5.8% with sixteen, on upstream 0afc417 with
GCC 13.3. A/A runs of the base binary moved the same median by 0.6% or
less.

Co-authored-by: Daejun Park <daejun7.park@samsung.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI/Build documentation Improvements or additions to documentation P1 run-ci size/L 1001+ changed lines (additions + deletions) Store Transfer Engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants