Skip to content

[serve] Columnar zero-copy autoscaling-metrics ingest - #64281

Open
johntaylor-cell wants to merge 2 commits into
ray-project:masterfrom
johntaylor-cell:scaling-autoscaling-metrics
Open

[serve] Columnar zero-copy autoscaling-metrics ingest#64281
johntaylor-cell wants to merge 2 commits into
ray-project:masterfrom
johntaylor-cell:scaling-autoscaling-metrics

Conversation

@johntaylor-cell

@johntaylor-cell johntaylor-cell commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Why are these changes needed?

The ServeController aggregates autoscaling metrics in a single-threaded control loop.
In aggregate mode with metrics collected on handles, each handle report covers every
replica it routes to, so at high fan-in the controller spends a large share of the loop
just cloudpickle-decoding those reports before it can aggregate them.

This PR serializes the reports columnarly (flat float64 arrays) and ingests them
natively with numpy, replacing the cloudpickle path for wide handle reports. numpy +
zlib release the GIL, so the decode overlaps the rest of the loop instead of serializing
in front of it.

Performance

Controller-scaling benchmark (handle-collection, HAProxy-on), cloudpickle vs columnar:

replicas / handle ingest ms/s (cloudpickle → columnar) effective per-loop cost speedup
64 29 → 4 114 → 41 2.8×
256 507 → 19 663 → 49 13.5×
1024 752 → 128 944 → 128 7.4×
4096 773 → 48 1228 → 212 5.8×

Cloudpickle ingest cost explodes with fan-in (it's serial in front of the loop); columnar
keeps it small and GIL-free, so the effective per-loop cost is max(loop, ingest)
instead of loop + ingest. The crossover is ~64 replicas/handle, which is exactly what
the width gate targets.

Handle-report ingest cost: cloudpickle vs columnar, and effective per-loop cost

(At a pinned steady-state fleet the loop isn't ingest-bound, so columnar alone is
~neutral on loop there and contributes mainly via lower deployment-state cost; the win
above is the high-fan-in ingest regime it targets.)

What changed

  • Width gate (RAY_SERVE_COLUMNAR_METRICS_MIN_REPLICAS, default 64): producers pick
    columnar only for handle reports covering ≥ the gate's replica count — the measured
    decode/merge crossover. Thin handle reports and all replica reports stay on the
    cloudpickle/object path, so small deployments never regress. Format is chosen by report
    type + width, not a separate on/off flag.
  • Wire-detected format: the controller routes on the SCR1 magic (framed outside
    zlib, O(1) detect), independent of how a report was produced — so a fleet mid-rollout
    (mixed columnar/cloudpickle senders) aggregates correctly.
  • Columnar payload: carries running_requests on the hot path plus custom
    autoscaling metrics in a side array store, exact-equivalent to the object path. numpy
    imports lazily, so serve-minimal installs (which never hit the columnar path) are
    unaffected.

An earlier revision also gated this behind an on/off RAY_SERVE_COLUMNAR_METRICS flag
(default off); that flag has been removed now that the path is proven — columnar is
selected purely by report type + the width gate. The width threshold stays env-tunable.

Correctness (mixed rollout)

  • Dedup-at-write: a source lives in exactly one store; switching wire format clears
    the other, so a mixed fleet never double-counts.
  • Unified cross-format staleness gate: one per-source last-accepted timestamp gates
    both the object and columnar ingest paths (per replica and per handle), so a delayed
    report in either format can't overwrite fresher data the other wrote.
  • Running-replica gate: the columnar aggregate keys off whether a running replica
    reported (not merely a non-empty array store), so a stale stopped-replica array left
    before on_replica_stopped can't suppress handle-collected running metrics — matching
    the object path exactly.

Test Plan

Unit suites: columnar wire-format + width gate, columnar-vs-object equivalence (every
aggregation function), controller ingest dispatch (aggregate vs simple mode), custom
metrics, mixed-rollout aggregation, cross-format staleness, and the stale-replica-array
regression.

An attached html document lists all of the optimizations that were implemented as part of a larger Serve controller scaling effort. This PR is for optimization A5.

image

@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 a columnar serialization format and array-native reference merge operations for Ray Serve's autoscaling metrics, replacing cloudpickle-based serialization with zero-copy numpy/array-based encoding to reduce CPU and latency overhead on the controller's event loop. It also implements reconciler optimizations, including a 'dirty-set' replica tracking mechanism to avoid container churn and an autoscale aggregation cache. The review feedback suggests optimizing the compression level in _frame from level=9 to a lower level to reduce CPU overhead, adding defensive length checks in the decode function to prevent crashes on corrupted payloads, and addressing the unused ready_set parameter and _ref_ready helper in deployment_state.py to improve maintainability.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread python/ray/serve/_private/autoscaling_metrics_codec.py Outdated
Comment thread python/ray/serve/_private/autoscaling_metrics_codec.py
Comment thread python/ray/serve/_private/deployment_state.py Outdated
Comment thread python/ray/serve/_private/autoscaling_state.py
Comment thread python/ray/serve/_private/autoscaling_state.py Outdated
Comment thread python/ray/serve/_private/autoscaling_state.py
@ray-gardener ray-gardener Bot added the serve Ray Serve Related Issue label Jun 23, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had
any activity for 14 days. It will be closed in another 14 days if no further activity occurs.
Thank you for your contributions.

You can always ask for help on our discussion forum or Ray's public slack channel.

If you'd like to keep this open, just leave any comment, and the stale label will be removed.

@github-actions github-actions Bot added the stale The issue is stale. It will be closed within 7 days unless there are further conversation label Jul 8, 2026
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 13d1343 to 01f1f4e Compare July 8, 2026 02:12
Comment thread python/ray/serve/_private/router.py Outdated
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 01f1f4e to 6ff7eb6 Compare July 8, 2026 02:48
@github-actions github-actions Bot added unstale A PR that has been marked unstale. It will not get marked stale again if this label is on it. and removed stale The issue is stale. It will be closed within 7 days unless there are further conversation labels Jul 8, 2026
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 6ff7eb6 to 12ad2e6 Compare July 8, 2026 15:53
Comment thread python/ray/serve/_private/autoscaling_state.py
Comment thread python/ray/serve/_private/autoscaling_state.py
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch 3 times, most recently from bb930db to ecde989 Compare July 8, 2026 16:41
Comment thread python/ray/serve/_private/autoscaling_state.py Outdated
@johntaylor-cell johntaylor-cell changed the title [serve] Controller scaling: reconciler loop + columnar autoscaling-me… [serve] Columnar autoscaling-metrics ingest (opt-in, width-gated) Jul 8, 2026
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from ecde989 to be4e601 Compare July 9, 2026 01:30
@johntaylor-cell johntaylor-cell added the go add ONLY when ready to merge, run all tests label Jul 9, 2026
Comment thread python/ray/serve/_private/autoscaling_state.py Outdated
Comment thread python/ray/serve/_private/controller.py
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch 2 times, most recently from d3da74a to 1b59c81 Compare July 9, 2026 19:21
@johntaylor-cell johntaylor-cell changed the title [serve] Columnar autoscaling-metrics ingest (opt-in, width-gated) [serve] Columnar autoscaling-metrics ingest (A5) Jul 9, 2026
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 1b59c81 to f2a7fb6 Compare July 9, 2026 20:36
Comment thread python/ray/serve/_private/autoscaling_metrics_codec.py
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 75d25c4 to 5c7a781 Compare July 11, 2026 23:57
Comment thread python/ray/serve/_private/autoscaling_state.py Outdated
@johntaylor-cell

Copy link
Copy Markdown
Contributor Author

@johntaylor-cell johntaylor-cell changed the title [serve] Columnar autoscaling-metrics ingest (A5) [serve] Columnar autoscaling-metrics ingest Jul 14, 2026
johntaylor-cell added a commit to johntaylor-cell/ray that referenced this pull request Jul 16, 2026
…y-project#64281)

Encode per-replica autoscaling metric timeseries as columnar numpy arrays instead of per-object cloudpickle when RAY_SERVE_COLUMNAR_METRICS is set (width-gated): array-based merge/aggregation, dual-store in DeploymentAutoscalingState, controller-side format detection, custom-metric support, and a unified cross-format staleness gate for replica + handle ingest.

Signed-off-by: john.taylor <john.taylor@anyscale.com>
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch 2 times, most recently from 5f0fc40 to 6c1a549 Compare July 18, 2026 16:04
@johntaylor-cell johntaylor-cell self-assigned this Jul 20, 2026
@johntaylor-cell johntaylor-cell changed the title [serve] Columnar autoscaling-metrics ingest [serve] Columnar zero-copy autoscaling-metrics ingest Jul 21, 2026
johntaylor-cell added a commit to johntaylor-cell/ray that referenced this pull request Jul 24, 2026
…y-project#64281)

Handles serialize wide (>= RAY_SERVE_COLUMNAR_METRICS_MIN_REPLICAS) metric reports
as flat float64 arrays (SCR1-framed); the controller wire-detects the format and
ingests via numpy into array stores; the aggregate-mode decision merge runs a numpy
port of the Cython kernels (exact-equivalent, randomized-verified). Opt-in via
RAY_SERVE_COLUMNAR_METRICS. Mixed columnar/object fleets aggregate exactly via
dedup-at-write + a unified per-source staleness timestamp.

Producers only choose columnar when numpy is importable AND the controller
aggregates raw timeseries (aggregate mode / direct ingress) -- in simple mode the
controller would reconstruct() every frame at pure overhead. Mixed-fleet
aggregation runs one fused array merge (wide columnar sources sliced as views,
thin object sources converted), so a steady mixed state (e.g. a thin driver
handle alongside wide proxy handles) never re-materializes arrays into per-point
objects. Malformed frames fail at ingest (ragged-index bounds check; zlib errors
surface as ValueError).

Behavior note: a present-but-empty replica running series no longer counts as
"metrics collected on replicas" in the object path (it previously suppressed
handle-collected running requests); this aligns the object, columnar, and mixed
paths and is regression-tested.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: john.taylor <john.taylor@anyscale.com>
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 6c1a549 to 48f5e10 Compare July 24, 2026 19:53
johntaylor-cell added a commit to johntaylor-cell/ray that referenced this pull request Jul 24, 2026
…y-project#64281)

Handles serialize wide (>= RAY_SERVE_COLUMNAR_METRICS_MIN_REPLICAS) metric reports
as flat float64 arrays (SCR1-framed); the controller wire-detects the format and
ingests via numpy into array stores; the aggregate-mode decision merge runs a numpy
port of the Cython kernels (exact-equivalent, randomized-verified). Opt-in via
RAY_SERVE_COLUMNAR_METRICS. Mixed columnar/object fleets aggregate exactly via
dedup-at-write + a unified per-source staleness timestamp.

Producers only choose columnar when numpy is importable AND the controller
aggregates raw timeseries (aggregate mode / direct ingress) -- in simple mode the
controller would reconstruct() every frame at pure overhead. Mixed-fleet
aggregation runs one fused array merge (wide columnar sources sliced as views,
thin object sources converted), so a steady mixed state (e.g. a thin driver
handle alongside wide proxy handles) never re-materializes arrays into per-point
objects. Malformed frames fail at ingest (ragged-index bounds check; zlib errors
surface as ValueError).

Behavior note: a present-but-empty replica running series no longer counts as
"metrics collected on replicas" in the object path (it previously suppressed
handle-collected running requests); this aligns the object, columnar, and mixed
paths and is regression-tested.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: john.taylor <john.taylor@anyscale.com>
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 48f5e10 to 6729383 Compare July 24, 2026 20:00
johntaylor-cell added a commit to johntaylor-cell/ray that referenced this pull request Jul 24, 2026
…t#64281)

Handles serialize wide (>= RAY_SERVE_COLUMNAR_METRICS_MIN_REPLICAS, default 64)
metric reports as flat float64 arrays (SCR1-framed); the controller wire-detects the
format and ingests via numpy into array stores; the aggregate-mode decision merge
runs a numpy port of the Cython kernels (exact-equivalent, randomized-verified).
Unconditional (no on/off flag): producers choose columnar whenever numpy is
importable, the controller aggregates raw timeseries (aggregate mode / direct
ingress), and the report clears the width gate; thin reports and replica reports
stay on the object path. Mixed columnar/object fleets aggregate exactly via
dedup-at-write + a unified per-source staleness timestamp, in one fused array merge
(wide columnar sources sliced as views, thin object sources converted) -- a steady
mixed state never re-materializes arrays into per-point objects.

Robustness/consistency: malformed frames fail at ingest (ragged-index bounds check;
zlib errors surface as ValueError); reconstruct() is a faithful inverse (NaN padding
never injects aggregated_metrics keys); the Prometheus metrics-delay histograms are
emitted on every ingest path; stale stopped-replica arrays cannot suppress
handle-collected running metrics; a present-but-empty replica running series does
not count as "metrics collected on replicas" (object, columnar, and mixed paths
agree; regression-tested).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: john.taylor <john.taylor@anyscale.com>
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from 6729383 to c50ecb8 Compare July 24, 2026 20:24
…t#64281)

Handles serialize wide (>= RAY_SERVE_COLUMNAR_METRICS_MIN_REPLICAS, default 64)
metric reports as flat float64 arrays (SCR1-framed); the controller wire-detects the
format and ingests via numpy into array stores; the aggregate-mode decision merge
runs a numpy port of the Cython kernels (exact-equivalent, randomized-verified).
Unconditional (no on/off flag): producers choose columnar whenever numpy is
importable, the controller aggregates raw timeseries (aggregate mode / direct
ingress), and the report clears the width gate; thin reports and replica reports
stay on the object path. Mixed columnar/object fleets aggregate exactly via
dedup-at-write + a unified per-source staleness timestamp, in one fused array merge
(wide columnar sources sliced as views, thin object sources converted) -- a steady
mixed state never re-materializes arrays into per-point objects.

Robustness/consistency: malformed frames fail at ingest (ragged-index bounds check;
zlib errors surface as ValueError); reconstruct() is a faithful inverse (NaN padding
never injects aggregated_metrics keys); the Prometheus metrics-delay histograms are
emitted on every ingest path; stale stopped-replica arrays cannot suppress
handle-collected running metrics; a present-but-empty replica running series does
not count as "metrics collected on replicas" (object, columnar, and mixed paths
agree; regression-tested).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: john.taylor <john.taylor@anyscale.com>
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from c50ecb8 to cca11ea Compare July 25, 2026 02:43
Comment thread python/ray/serve/_private/autoscaling_metrics_merge.py Outdated
The array merge rounded timestamps in two places the object path does not:

- a lone active source was rounded, where merge_instantaneous_total returns
  that series untouched (return active_series[0]);
- aligned_start was rounded, where the object path takes it from the ORIGINAL
  first timestamps. The comment at autoscaling_state.py:721 explains why that
  matters: the bound is compared against merged timestamps that ARE rounded to
  10ms, so rounding the bound too can shift which points fall in the window.

Multi-source rounding stays -- the object path's kernel rounds to 10ms and
collapses colliding stamps, so that part was already equivalent (verified
against the compiled kernel directly).

test_array_merge_matches_object_kernels could not catch this: it generates
timestamps as round(uniform(...), 2), which makes every round() in the array
path a no-op, and rounds its reference before comparing. Real time.time()
values are not 2-decimal.

The new test uses unrounded timestamps and compares against the PRODUCTION
method rather than a reimplementation of its window logic, with time.time()
pinned because _merge_and_aggregate_timeseries reads it internally (:711). It
fails on the pre-fix code and passes after.

Signed-off-by: john.taylor <john.taylor@anyscale.com>
@johntaylor-cell
johntaylor-cell force-pushed the scaling-autoscaling-metrics branch from ec7a785 to c21fa24 Compare July 27, 2026 16:51

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit c21fa24. Configure here.

Comment thread python/ray/serve/_private/autoscaling_metrics_codec.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go add ONLY when ready to merge, run all tests performance serve Ray Serve Related Issue unstale A PR that has been marked unstale. It will not get marked stale again if this label is on it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant