[core][taskEvents out of GCS][2.5/n] Optimize ray task event recorder - #65288
[core][taskEvents out of GCS][2.5/n] Optimize ray task event recorder#65288karticam wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the task event recording and serialization logic. It extracts task event population helpers and the TaskStateUpdate struct into a separate observability library, and defers protobuf serialization for definition and lifecycle events to the export path, keeping it off the task's critical path. Additionally, it optimizes the task event buffer to only record events when at least one destination is active. The review feedback suggests several improvements: replacing std::make_move_iterator on a const reference with standard iterators, adding defensive null checks for task_spec in event constructors, replacing non-standard std::optional<const T> types with const std::optional<T>&, and simplifying implicit std::optional construction in unit tests.
| definition_event_data.mutable_required_resources()->insert( | ||
| std::make_move_iterator(required_resources.begin()), | ||
| std::make_move_iterator(required_resources.end())); |
There was a problem hiding this comment.
Since required_resources is a const reference, std::make_move_iterator cannot actually move the elements and will fall back to copying them. Using std::make_move_iterator on const iterators is misleading and can cause compiler warnings. We should use standard iterators instead.
| definition_event_data.mutable_required_resources()->insert( | |
| std::make_move_iterator(required_resources.begin()), | |
| std::make_move_iterator(required_resources.end())); | |
| definition_event_data.mutable_required_resources()->insert( | |
| required_resources.begin(), | |
| required_resources.end()); |
There was a problem hiding this comment.
Seems to be a valid comment
| task_spec_(std::move(task_spec)), | ||
| task_id_(task_id), | ||
| job_id_(job_id), | ||
| task_attempt_(task_attempt) {} |
| task_spec_(std::move(task_spec)), | ||
| task_id_(task_id), | ||
| job_id_(job_id), | ||
| task_attempt_(task_attempt) {} |
| const JobID &job_id, | ||
| int32_t task_attempt, | ||
| rpc::TaskStatus task_status, | ||
| const std::optional<const TaskStateUpdate> &state_update, |
There was a problem hiding this comment.
Using std::optional<const TaskStateUpdate> is non-standard and can lead to compilation issues on some compilers or standard library implementations, as std::optional is typically expected to hold non-const types. It also introduces inconsistency with task_event_populators.h which uses const std::optional<TaskStateUpdate>&. We should change this to const std::optional<TaskStateUpdate>&.
const std::optional<TaskStateUpdate> &state_update,| const JobID &job_id, | ||
| int32_t task_attempt, | ||
| rpc::TaskStatus task_status, | ||
| const std::optional<const TaskStateUpdate> &state_update, |
There was a problem hiding this comment.
Using std::optional<const TaskStateUpdate> is non-standard and can lead to compilation issues on some compilers or standard library implementations, as std::optional is typically expected to hold non-const types. It also introduces inconsistency with task_event_populators.h which uses const std::optional<TaskStateUpdate>&. We should change this to const std::optional<TaskStateUpdate>&.
| const std::optional<const TaskStateUpdate> &state_update, | |
| const std::optional<TaskStateUpdate> &state_update, |
| struct TaskStateUpdate { | ||
| TaskStateUpdate() = default; | ||
|
|
||
| explicit TaskStateUpdate(const std::optional<const rpc::RayErrorInfo> &error_info) |
There was a problem hiding this comment.
Using std::optional<const rpc::RayErrorInfo> is non-standard and can lead to compilation issues on some compilers or standard library implementations, as std::optional is typically expected to hold non-const types. We should change this to const std::optional<rpc::RayErrorInfo>&.
| explicit TaskStateUpdate(const std::optional<const rpc::RayErrorInfo> &error_info) | |
| explicit TaskStateUpdate(const std::optional<rpc::RayErrorInfo> &error_info) |
| job_id, | ||
| /*task_attempt=*/0, | ||
| rpc::TaskStatus::SUBMITTED_TO_WORKER, | ||
| std::optional<const TaskStateUpdate>(TaskStateUpdate(node_id, worker_id)), |
There was a problem hiding this comment.
Since std::optional can be implicitly constructed from its underlying type, we can simplify this by passing TaskStateUpdate directly without the explicit std::optional wrapper.
| std::optional<const TaskStateUpdate>(TaskStateUpdate(node_id, worker_id)), | |
| TaskStateUpdate(node_id, worker_id), |
| job_id, | ||
| /*task_attempt=*/0, | ||
| rpc::TaskStatus::RUNNING, | ||
| std::optional<const TaskStateUpdate>(TaskStateUpdate(static_cast<uint32_t>(4321))), |
There was a problem hiding this comment.
Since std::optional can be implicitly constructed from its underlying type, we can simplify this by passing TaskStateUpdate directly without the explicit std::optional wrapper.
| std::optional<const TaskStateUpdate>(TaskStateUpdate(static_cast<uint32_t>(4321))), | |
| TaskStateUpdate(static_cast<uint32_t>(4321)), |
| job_id, | ||
| /*task_attempt=*/0, | ||
| rpc::TaskStatus::NIL, | ||
| std::optional<const TaskStateUpdate>(TaskStateUpdate(start_log)), |
There was a problem hiding this comment.
| job_id, | ||
| /*task_attempt=*/0, | ||
| rpc::TaskStatus::NIL, | ||
| std::optional<const TaskStateUpdate>(TaskStateUpdate(end_log)), |
There was a problem hiding this comment.
| } | ||
|
|
||
| if (data->events_size() == 0 && metadata->dropped_task_attempts_size() == 0) { | ||
| MarkGrpcDone(); |
There was a problem hiding this comment.
PR needs clearer description
Low Severity
To help reviewers, please ensure your PR includes:
- Title: A concise summary of the change
- Description:
- What problem does this solve?
- How does this PR solve it?
- Any relevant context for reviewers such as:
- Why is the problem important to solve?
- Why was this approach chosen over others?
See this list of PRs as examples for PRs that have gone above and beyond:
- [Core] Introduce local port service discovery #59613
- [Core] Improve Large-Scale Resource View Synchronization Through Sync Message Batching #57641
- Remove node observability information from hot path of core components #56474
- [core][rdt] Support out-of-order actors by extracting metadata when creating #59610
- [core] fix open leak for plasma store memory (shm/fallback) by workers #52622
Triggered by project rule: Bugbot Rules
Reviewed by Cursor Bugbot for commit 414d343. Configure here.
414d343 to
7a2b482
Compare
MengjinYan
left a comment
There was a problem hiding this comment.
Done with opt1. Keeping looking at other opts
| const TaskAttemptId &attempt) | ||
| ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); | ||
|
|
||
| // Move the buffered events, and the dropped task attempts to report, out of the buffers |
There was a problem hiding this comment.
Nit: Use doxygen format for function comment.
| // so that the caller can group, serialize and send them without holding mutex_. Events | ||
| // belonging to a dropped attempt are discarded so that a task attempt is either sent in | ||
| // full or not at all. | ||
| void TakeEventsToSend(std::list<std::unique_ptr<RayEventInterface>> *events, |
There was a problem hiding this comment.
Minor: Prefer a return structure than output parameters.
| return; | ||
| } | ||
| // Skip if there's already an in-flight gRPC call to avoid overlapping requests. | ||
| if (grpc_in_progress_.exchange(true)) { |
There was a problem hiding this comment.
It seems that with the change, the behavior of when the grpc_in_progress_ is set is changed as well. Wondering is it the expected behavior? The same logic is in ray_event_recorder as well. So we might want to consolidate the logic for both places.
MengjinYan
left a comment
There was a problem hiding this comment.
Done with opt 2 and its fix. Moving on to opt 3
| : is_debugger_paused_(is_debugger_paused) {} | ||
|
|
||
| /// Node id if it's a SUBMITTED_TO_WORKER status change. | ||
| std::optional<NodeID> node_id_ = std::nullopt; |
There was a problem hiding this comment.
style: struct data members shouldn't have trailing underscore, per google c++ style guide.
| definition_event_data.mutable_required_resources()->insert( | ||
| std::make_move_iterator(required_resources.begin()), | ||
| std::make_move_iterator(required_resources.end())); |
There was a problem hiding this comment.
Seems to be a valid comment
|
|
||
| std::string RayActorTaskDefinitionEvent::GetEntityId() const { | ||
| return data_.task_id() + std::to_string(data_.task_attempt()); | ||
| return task_id_.Binary() + std::to_string(task_attempt_); |
There was a problem hiding this comment.
Prefer absl::StrCat() for string concatenation
There was a problem hiding this comment.
should we add tests for actor definition event as well?
MengjinYan
left a comment
There was a problem hiding this comment.
Opt3 looks good. Since the new task event recorder will replace the task event buffer in the near future, it's okay for now to add an additional boolean field in the class.
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
7a2b482 to
9edc5ae
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Reviewed by Cursor Bugbot for commit 9edc5ae. Configure here.
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
MengjinYan
left a comment
There was a problem hiding this comment.
All resolved comments looks good. One minor followup comment.
|
|
||
| std::string RayTaskLifecycleEvent::GetEntityId() const { | ||
| return data_.task_id() + std::to_string(data_.task_attempt()); | ||
| return task_id_.Binary() + std::to_string(task_attempt_); |
There was a problem hiding this comment.
minor: Prefer absl::StrCat here.
Signed-off-by: Kartica Modi <karticamodi@gmail.com>


When running benchmarks on #64835 with flags on that trigger the ray task event recorder flow - namely:
enable_ray_event:trueenable_ray_task_event_recorder:trueenable_core_worker_task_event_to_gcs:false(GCS fed only via recorder->aggregator)I found some regressions. This PR fixes those regressions.
There were 3 optimizations made to
ray_task_event_recorder.The 3 optimizations are applied as different commits in the PR (added below with the explanations). For easier review, go through the commits separately.
Optimizations:
c6efc55 [opt1] : Earlier all of
ExportEventsfunction operated under the mutex. The function would first process the status events and profile events along with skipped events to make form the object to send to aggregator agent. Then it would do group, merge and serialize those event and then send it to the aggregator agent. Given the max buffer size which could be 100k potentially, this could take a lot of time and holding the mutex for long could lead to otherAddEventsrequest to be stalled. The fix is to release the mutex as soon as the shared queues are drained out in local queues and then do the rest of the processing.005e5d7 [opt2]: Earlier, the ray event protos for task status events (
RayTaskDefinitionEvent,RayActorTaskDefinitionEventandRayTaskLifecycleEvent) were made in the hot task execution path. This commit moves it to the export part. Since export runs on on a different thread, it doesn't hamper task execution.RayTaskDefinitionEventandRayActorTaskDefinitionEvent, task event recorder'sMergeDatais a no-op and therefore,SerializeDatacan populate the event proto trivially (just call the populate function fromSerializeData)., we collate all the state updates in 'MergeDataand then inSerializeData, apply each state update one by one.Note that this thing has one caveat that now the proto formation happens in
ExportEventsunder the task event recorder mutex, while earlier they were out of the mutex (it used to happen before callingAddEventon the proto). So this increases mutex contention.To try to fix this, I tried to export less than the entire buffer at a time (say 10k out of the 100k buffer) so that there is less work under the mutex. But firstly, this led to poorer perf as compared to flushing the entire buffer and secondly to fully get it to work around all cases, some other changes were required in the recorder as well.
task_event_bufferto GCS or aggregator flow was turned off, the current code did form theTaskStatusEventand put it in the ring buffer under mutex. The fix made is that if ``task_event_buffer` is not publishing anywhere, short circuit that path very early.Results:
Given below are the regression % after before any optimizations and after each optimization.
1_1_actor_calls_async1_n_actor_calls_async1_n_async_actor_calls_async1_1_actor_calls_concurrentn_n_actor_calls_asyncn_n_async_actor_calls_asyncsingle_client_tasks_and_get_batchsingle_client_tasks_async1_1_async_actor_calls_asyncsingle_client_wait_1k_refsmulti_client_tasks_async1_1_async_actor_calls_with_args_async1_1_actor_calls_sync1_1_async_actor_calls_syncsingle_client_tasks_syncThere are two other optimizations that I tried which are not a part of this PR:
(task-id-binary-string, attempt),key using(TaskID, attempt)to prevent string allocations.shared_ptrinstead of copying it into each event.But it changes several widely-used signatures (TaskAttemptId, GetSessionName, the record entry point). Since it gives ~2.5% gain and we are already below 8% I didn't include it in this PR.
MetricInterfacewhich does some allocations etc. holding a mutex. This could increase contention over the mutex. I tried to batch the drops and then export them to the metric only duringExportEvents. This gave some perf gains (when I was trying flushing only 10k events per batch), but then it hampers metric fidelity since they are now stale between flushes.