[autoscaler] Precompute SerializeToString keys to eliminate redundant serialization in scheduling loop - #65282
Conversation
…g hotspot
In a single scheduling round, every request object is serialized via
SerializeToString(deterministic=True) once per candidate node inside
try_schedule, resulting in N_nodes × M_requests redundant serialization
calls. For 15000 pending requests across 3000 nodes, this produces
millions of repeated protobuf serializations consuming 55%+ of scheduler
CPU time.
Fix: precompute a {id(request): bytes} dictionary once before entering
the scheduling loop and pass it through _sched_best_node → try_schedule.
UnschedulableRequestCache is simplified to accept raw bytes keys.
Signed-off-by: wangjia23 <wangjia23@xiaomi.com>
…docstring - Use shape_keys.get(id(r)) with fallback to SerializeToString when key is missing, preventing potential KeyError if request objects are ever introduced outside the precomputed dict - Add shape_keys parameter documentation to _sched_best_node docstring Signed-off-by: wangjia23 <wangjia23@xiaomi.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: wangjia23 <wangjia23@xiaomi.com>
There was a problem hiding this comment.
Code Review
This pull request optimizes the scheduling logic in python/ray/autoscaler/v2/scheduler.py by precomputing serialization keys (shape_keys) for resource requests, which avoids redundant SerializeToString calls inside the per-node scheduling loop. Additionally, UnschedulableRequestCache is simplified to cache and check these serialized shape keys directly. The feedback suggests simplifying the logic for retrieving or computing the serialization key sk inside try_schedule to eliminate duplicate calls to SerializeToString and improve readability.
| if shape_keys is not None: | ||
| sk = shape_keys.get(id(r)) | ||
| if sk is None: | ||
| sk = r.SerializeToString(deterministic=True) | ||
| else: | ||
| sk = r.SerializeToString(deterministic=True) |
There was a problem hiding this comment.
The logic for retrieving or computing the serialization key sk can be simplified to avoid duplicating the r.SerializeToString(deterministic=True) call. This improves readability and maintainability of the code.
sk = shape_keys.get(id(r)) if shape_keys is not None else None
if sk is None:
sk = r.SerializeToString(deterministic=True)- test_precomputed_keys_produce_same_result_as_fallback: verifies that passing shape_keys produces identical scheduling results as the None fallback path - test_precomputed_keys_fallback_on_missing_id: verifies defensive .get() fallback when request id is not in the precomputed dict - test_unschedulable_request_cache_bytes_interface: direct unit test for the simplified UnschedulableRequestCache bytes-based API - test_precomputed_keys_with_duplicate_objects: verifies correctness when the same request object appears multiple times (ungroup_by_count pattern) - test_precomputed_keys_end_to_end_scheduling: full scheduling round confirming correct launch decisions with the optimization active Signed-off-by: wangjia23 <wangjia23@xiaomi.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: wangjia23 <wangjia23@xiaomi.com>
Address review feedback from gemini-code-assist: consolidate the shape_keys retrieval and fallback into two lines instead of a nested if/else with duplicated SerializeToString calls. Signed-off-by: wangjia23 <wangjia23@xiaomi.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: wangjia23 <wangjia23@xiaomi.com>
…pe from docstrings - Add # noqa to dict comprehension line that triggers the docstyle regex (id(r): ... matches the "param (Type): desc" pattern as a false positive) - Remove type annotations from shape_keys docstring descriptions per Ray Google pydoc style convention Signed-off-by: wangjia23 <wangjia23@xiaomi.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: wangjia23 <wangjia23@xiaomi.com>
Why are these changes needed?
After merging #64175, benchmark testing showed that scaling to 3000 nodes (15000 tasks × 0.2 CPU + 30MB memory) still takes ~51 minutes. Profiling with py-spy revealed two independent hotspots in the scheduling loop:
SerializeToStringcalls — fixed in this PRThe
UnschedulableRequestCache.contains()serializes each request viaSerializeToString(deterministic=True)once per candidate node insidetry_schedule, resulting in N_nodes × M_requests redundant serializations per scheduling round. For 15000 requests across 3000 nodes, this produces ~45M repeated protobuf serializations.Profiling evidence (py-spy, 30s sample during baseline run):
contains()in scheduler.py: 55% Own CPU (7.77s / 14s)_audit_fork_safetytriggered by SerializeToString insidecontains(): 18% Own CPU (2.43s / 14s)Fix
Precompute a
{id(request): bytes}dictionary once before entering the scheduling loop, and pass it through_sched_best_node→try_schedule. TheUnschedulableRequestCacheinterface is simplified to accept rawbyteskeys directly.Includes a defensive
.get()fallback: if a request is not in the precomputed dict, it falls back to on-the-fly serialization rather than raising aKeyError.Benchmark
Cluster: 3000 max workers (1 CPU each), 15000 tasks × 0.2 CPU.
Related PRs
Checks
try_scheduleinterface change is backward-compatible viaOptionalparameter with on-the-fly fallback.python/ray/autoscaler/v2/scheduler.py(single file, low blast radius).