[serve] Columnar zero-copy autoscaling-metrics ingest - #64281
[serve] Columnar zero-copy autoscaling-metrics ingest#64281johntaylor-cell wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
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.
|
This pull request has been automatically marked as stale because it has not had 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. |
13d1343 to
01f1f4e
Compare
01f1f4e to
6ff7eb6
Compare
6ff7eb6 to
12ad2e6
Compare
bb930db to
ecde989
Compare
ecde989 to
be4e601
Compare
d3da74a to
1b59c81
Compare
1b59c81 to
f2a7fb6
Compare
75d25c4 to
5c7a781
Compare
…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>
5f0fc40 to
6c1a549
Compare
…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>
6c1a549 to
48f5e10
Compare
…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>
48f5e10 to
6729383
Compare
…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>
6729383 to
c50ecb8
Compare
…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>
c50ecb8 to
cca11ea
Compare
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>
ec7a785 to
c21fa24
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Reviewed by Cursor Bugbot for commit c21fa24. Configure here.

Why are these changes needed?
The
ServeControlleraggregates 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
float64arrays) and ingests themnatively 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:
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 whatthe width gate targets.
(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
RAY_SERVE_COLUMNAR_METRICS_MIN_REPLICAS, default 64): producers pickcolumnar 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.
SCR1magic (framed outsidezlib, O(1) detect), independent of how a report was produced — so a fleet mid-rollout
(mixed columnar/cloudpickle senders) aggregates correctly.
running_requestson the hot path plus customautoscaling 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_METRICSflag(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)
the other, so a mixed fleet never double-counts.
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.
reported (not merely a non-empty array store), so a stale stopped-replica array left
before
on_replica_stoppedcan't suppress handle-collected running metrics — matchingthe 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.