-
Notifications
You must be signed in to change notification settings - Fork 7.8k
[core] Admission control for pulling objects to the local node #13514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
91a1d1f
136574c
4f38ef4
cc34ea2
e1edc19
70cf694
add49bd
1e9d091
987d710
b522cd4
03ffed7
2bc1cee
0070cfa
9114c94
7662e14
73375ce
ce6d79b
68c9efd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -296,6 +296,83 @@ def driver(): | |
| ray.get(driver.remote()) | ||
|
|
||
|
|
||
| @pytest.mark.timeout(30) | ||
| def test_pull_bundles_admission_control(shutdown_only): | ||
| cluster = Cluster() | ||
| object_size = int(1e7) | ||
| num_objects = 10 | ||
| num_tasks = 10 | ||
| # Head node can fit all of the objects at once. | ||
|
rkooo567 marked this conversation as resolved.
|
||
| cluster.add_node( | ||
| num_cpus=0, | ||
| object_store_memory=2 * num_tasks * num_objects * object_size) | ||
| cluster.wait_for_nodes() | ||
| ray.init(address=cluster.address) | ||
|
|
||
| # Worker node can only fit 1 task at a time. | ||
| cluster.add_node( | ||
| num_cpus=1, object_store_memory=1.5 * num_objects * object_size) | ||
| cluster.wait_for_nodes() | ||
|
|
||
| @ray.remote | ||
| def foo(*args): | ||
| return | ||
|
|
||
| args = [] | ||
| for _ in range(num_tasks): | ||
| task_args = [ | ||
| ray.put(np.zeros(object_size, dtype=np.uint8)) | ||
| for _ in range(num_objects) | ||
| ] | ||
| args.append(task_args) | ||
|
|
||
| tasks = [foo.remote(*task_args) for task_args in args] | ||
| ray.get(tasks) | ||
|
|
||
|
|
||
| @pytest.mark.timeout(30) | ||
| def test_pull_bundles_admission_control_dynamic(shutdown_only): | ||
| # This test is the same as test_pull_bundles_admission_control, except that | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a nice test! |
||
| # the object store's capacity starts off higher and is later consumed | ||
| # dynamically by concurrent workers. | ||
| cluster = Cluster() | ||
| object_size = int(1e7) | ||
| num_objects = 10 | ||
| num_tasks = 10 | ||
| # Head node can fit all of the objects at once. | ||
| cluster.add_node( | ||
| num_cpus=0, | ||
| object_store_memory=2 * num_tasks * num_objects * object_size) | ||
| cluster.wait_for_nodes() | ||
| ray.init(address=cluster.address) | ||
|
|
||
| # Worker node can fit 2 tasks at a time. | ||
| cluster.add_node( | ||
| num_cpus=1, object_store_memory=2.5 * num_objects * object_size) | ||
| cluster.wait_for_nodes() | ||
|
|
||
| @ray.remote | ||
| def foo(*args): | ||
| return | ||
|
|
||
| @ray.remote | ||
| def allocate(*args): | ||
| return np.zeros(object_size, dtype=np.uint8) | ||
|
|
||
| args = [] | ||
| for _ in range(num_tasks): | ||
| task_args = [ | ||
| ray.put(np.zeros(object_size, dtype=np.uint8)) | ||
| for _ in range(num_objects) | ||
| ] | ||
| args.append(task_args) | ||
|
|
||
| tasks = [foo.remote(*task_args) for task_args in args] | ||
| allocated = [allocate.remote() for _ in range(num_objects)] | ||
| ray.get(tasks) | ||
| del allocated | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| import pytest | ||
| import sys | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -397,6 +397,12 @@ class ReferenceCounter : public ReferenceCounterInterface, | |
| absl::optional<absl::flat_hash_set<NodeID>> GetObjectLocations( | ||
| const ObjectID &object_id) LOCKS_EXCLUDED(mutex_); | ||
|
|
||
| /// Get an object's size. This will return 0 if the object is out of scope. | ||
| /// | ||
| /// \param[in] object_id The object whose size to get. | ||
| /// \return Object size, or 0 if the object is out of scope. | ||
| size_t GetObjectSize(const ObjectID &object_id) const; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you write a comment? (and explain when 0 is returned?) |
||
|
|
||
| /// Handle an object has been spilled to external storage. | ||
| /// | ||
| /// This notifies the primary raylet that the object is safe to release and | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,13 +31,21 @@ using ray::rpc::ObjectTableData; | |
| /// object table entries up to but not including this notification. | ||
| bool UpdateObjectLocations(const std::vector<rpc::ObjectLocationChange> &location_updates, | ||
| std::shared_ptr<gcs::GcsClient> gcs_client, | ||
| std::unordered_set<NodeID> *node_ids, | ||
| std::string *spilled_url) { | ||
| std::unordered_set<NodeID> *node_ids, std::string *spilled_url, | ||
| size_t *object_size) { | ||
| // location_updates contains the updates of locations of the object. | ||
| // with GcsChangeMode, we can determine whether the update mode is | ||
| // addition or deletion. | ||
| bool isUpdated = false; | ||
| for (const auto &update : location_updates) { | ||
| // The size can be 0 if the update was a deletion. This assumes that an | ||
| // object's size is always greater than 0. | ||
| // TODO(swang): If that's not the case, we should use a flag to check | ||
| // whether the size is set instead. | ||
| if (update.size() > 0) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When's the case where update.size is not bigger than 0?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's 0 for deletion. We can add a flag instead if there are cases where the object size really is 0.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mind writing a comment here? |
||
| *object_size = update.size(); | ||
| } | ||
|
|
||
| if (!update.node_id().empty()) { | ||
| NodeID node_id = NodeID::FromBinary(update.node_id()); | ||
| if (update.is_add() && 0 == node_ids->count(node_id)) { | ||
|
|
@@ -73,9 +81,10 @@ bool UpdateObjectLocations(const std::vector<rpc::ObjectLocationChange> &locatio | |
| ray::Status ObjectDirectory::ReportObjectAdded( | ||
| const ObjectID &object_id, const NodeID &node_id, | ||
| const object_manager::protocol::ObjectInfoT &object_info) { | ||
| RAY_LOG(DEBUG) << "Reporting object added to GCS " << object_id; | ||
| size_t size = object_info.data_size + object_info.metadata_size; | ||
| RAY_LOG(DEBUG) << "Reporting object added to GCS " << object_id << " size " << size; | ||
| ray::Status status = | ||
| gcs_client_->Objects().AsyncAddLocation(object_id, node_id, nullptr); | ||
| gcs_client_->Objects().AsyncAddLocation(object_id, node_id, size, nullptr); | ||
| return status; | ||
| } | ||
|
|
||
|
|
@@ -119,14 +128,14 @@ void ObjectDirectory::HandleNodeRemoved(const NodeID &node_id) { | |
| // If the subscribed object has the removed node as a location, update | ||
| // its locations with an empty update so that the location will be removed. | ||
| UpdateObjectLocations({}, gcs_client_, &listener.second.current_object_locations, | ||
| &listener.second.spilled_url); | ||
| &listener.second.spilled_url, &listener.second.object_size); | ||
| // Re-call all the subscribed callbacks for the object, since its | ||
| // locations have changed. | ||
| for (const auto &callback_pair : listener.second.callbacks) { | ||
| // It is safe to call the callback directly since this is already running | ||
| // in the subscription callback stack. | ||
| callback_pair.second(object_id, listener.second.current_object_locations, | ||
| listener.second.spilled_url); | ||
| listener.second.spilled_url, listener.second.object_size); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -157,7 +166,7 @@ ray::Status ObjectDirectory::SubscribeObjectLocations(const UniqueID &callback_i | |
| // Update entries for this object. | ||
| if (!UpdateObjectLocations(object_notifications, gcs_client_, | ||
| &it->second.current_object_locations, | ||
| &it->second.spilled_url)) { | ||
| &it->second.spilled_url, &it->second.object_size)) { | ||
| return; | ||
| } | ||
| // Copy the callbacks so that the callbacks can unsubscribe without interrupting | ||
|
|
@@ -171,7 +180,7 @@ ray::Status ObjectDirectory::SubscribeObjectLocations(const UniqueID &callback_i | |
| // It is safe to call the callback directly since this is already running | ||
| // in the subscription callback stack. | ||
| callback_pair.second(object_id, it->second.current_object_locations, | ||
| it->second.spilled_url); | ||
| it->second.spilled_url, it->second.object_size); | ||
| } | ||
| }; | ||
| status = gcs_client_->Objects().AsyncSubscribeToLocations( | ||
|
|
@@ -189,8 +198,9 @@ ray::Status ObjectDirectory::SubscribeObjectLocations(const UniqueID &callback_i | |
| if (listener_state.subscribed) { | ||
| auto &locations = listener_state.current_object_locations; | ||
| auto &spilled_url = listener_state.spilled_url; | ||
| io_service_.post([callback, locations, spilled_url, object_id]() { | ||
| callback(object_id, locations, spilled_url); | ||
| auto object_size = it->second.object_size; | ||
| io_service_.post([callback, locations, spilled_url, object_size, object_id]() { | ||
| callback(object_id, locations, spilled_url, object_size); | ||
| }); | ||
| } | ||
| return status; | ||
|
|
@@ -223,8 +233,9 @@ ray::Status ObjectDirectory::LookupLocations(const ObjectID &object_id, | |
| // cached locations. | ||
| auto &locations = it->second.current_object_locations; | ||
| auto &spilled_url = it->second.spilled_url; | ||
| io_service_.post([callback, object_id, spilled_url, locations]() { | ||
| callback(object_id, locations, spilled_url); | ||
| auto object_size = it->second.object_size; | ||
| io_service_.post([callback, object_id, spilled_url, locations, object_size]() { | ||
| callback(object_id, locations, spilled_url, object_size); | ||
| }); | ||
| } else { | ||
| // We do not have any locations cached due to a concurrent | ||
|
|
@@ -252,10 +263,12 @@ ray::Status ObjectDirectory::LookupLocations(const ObjectID &object_id, | |
|
|
||
| std::unordered_set<NodeID> node_ids; | ||
| std::string spilled_url; | ||
| UpdateObjectLocations(notification, gcs_client_, &node_ids, &spilled_url); | ||
| size_t object_size = 0; | ||
| UpdateObjectLocations(notification, gcs_client_, &node_ids, &spilled_url, | ||
| &object_size); | ||
| // It is safe to call the callback directly since this is already running | ||
| // in the GCS client's lookup callback stack. | ||
| callback(object_id, node_ids, spilled_url); | ||
| callback(object_id, node_ids, spilled_url, object_size); | ||
| }); | ||
| } | ||
| return status; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we just use ray_start_cluster?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was too lazy to figure out how to parametrize it properly :D Also, I was running into trouble where the non-head node would connect first, so the rest of the test wouldn't run properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh lol. I think you can just do like