chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+714
View File
@@ -0,0 +1,714 @@
import json
import random
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import asdict
from unittest.mock import AsyncMock
import pytest
from click.testing import CliRunner
import ray
from ray._common.test_utils import wait_for_condition
from ray._private.test_utils import wait_for_aggregator_agent_if_enabled
from ray._raylet import ActorID, NodeID, ObjectID, TaskID
from ray.core.generated.common_pb2 import TaskStatus, TaskType, WorkerType
from ray.core.generated.gcs_pb2 import ActorTableData, GcsNodeInfo
from ray.core.generated.gcs_service_pb2 import GetAllActorInfoReply
from ray.core.generated.node_manager_pb2 import GetObjectsInfoReply
from ray.dashboard.state_aggregator import StateAPIManager
from ray.tests.test_state_api import (
generate_actor_data,
generate_object_info,
generate_task_data,
generate_task_event,
)
from ray.util.state import (
summarize_actors,
summarize_objects,
summarize_tasks,
)
from ray.util.state.common import (
DEFAULT_RPC_TIMEOUT,
DRIVER_TASK_ID_PREFIX,
Link,
NestedTaskSummary,
SummaryApiOptions,
TaskSummaries,
)
from ray.util.state.state_cli import summary_state_cli_group
from ray.util.state.state_manager import StateDataSourceClient
@pytest.fixture
def state_api_manager():
data_source_client = AsyncMock(StateDataSourceClient)
manager = StateAPIManager(
data_source_client, thread_pool_executor=ThreadPoolExecutor()
)
yield manager
def create_summary_options(
timeout: int = DEFAULT_RPC_TIMEOUT,
):
return SummaryApiOptions(timeout=timeout)
@pytest.mark.asyncio
async def test_api_manager_summary_tasks(state_api_manager):
data_source_client = state_api_manager.data_source_client
first_task_name = "1"
second_task_name = "2"
data_source_client.get_all_task_info = AsyncMock()
ids = [TaskID((f"{i}" * 24).encode()) for i in range(5)]
# 1: {PENDING_NODE_ASSIGNMENT:3, RUNNING:1}, 2:{PENDING_NODE_ASSIGNMENT: 1}
data_source_client.get_all_task_info.side_effect = [
generate_task_data(
[
generate_task_event(
id=ids[0].binary(),
name="",
func_or_class=first_task_name,
state=TaskStatus.PENDING_NODE_ASSIGNMENT,
type=TaskType.NORMAL_TASK,
),
generate_task_event(
id=ids[1].binary(),
name="",
func_or_class=first_task_name,
state=TaskStatus.PENDING_NODE_ASSIGNMENT,
type=TaskType.NORMAL_TASK,
),
generate_task_event(
id=ids[2].binary(),
name="",
func_or_class=first_task_name,
state=TaskStatus.PENDING_NODE_ASSIGNMENT,
type=TaskType.NORMAL_TASK,
),
generate_task_event(
id=ids[3].binary(),
name="",
func_or_class=first_task_name,
state=TaskStatus.RUNNING,
type=TaskType.NORMAL_TASK,
),
generate_task_event(
id=ids[4].binary(),
name="",
func_or_class=second_task_name,
state=TaskStatus.PENDING_NODE_ASSIGNMENT,
type=TaskType.ACTOR_TASK,
),
]
)
]
"""
Test cluster summary.
"""
result = await state_api_manager.summarize_tasks(option=create_summary_options())
assert "cluster" in result.result.node_id_to_summary
data = result.result.node_id_to_summary["cluster"]
assert data.summary[first_task_name].type == "NORMAL_TASK"
assert data.summary[first_task_name].func_or_class_name == first_task_name
assert data.summary[first_task_name].state_counts["PENDING_NODE_ASSIGNMENT"] == 3
assert data.summary[first_task_name].state_counts["RUNNING"] == 1
assert data.summary[second_task_name].type == "ACTOR_TASK"
assert data.summary[second_task_name].func_or_class_name == second_task_name
assert data.summary[second_task_name].state_counts["PENDING_NODE_ASSIGNMENT"] == 1
assert data.total_tasks == 4
assert data.total_actor_tasks == 1
assert data.total_actor_scheduled == 0
"""
Test if it can be correctly modified to a dictionary.
"""
print(result.result)
result_in_dict = asdict(result.result)
assert json.loads(json.dumps(result_in_dict)) == result_in_dict
@pytest.mark.asyncio
async def test_api_manager_summary_actors(state_api_manager):
data_source_client = state_api_manager.data_source_client
actor_ids = [ActorID((f"{i}" * 16).encode()) for i in range(9)]
class_a = "A"
class_b = "B"
data_source_client.get_all_actor_info.return_value = GetAllActorInfoReply(
actor_table_data=[
generate_actor_data(
actor_ids[0].binary(),
state=ActorTableData.ActorState.ALIVE,
class_name=class_a,
),
generate_actor_data(
actor_ids[1].binary(),
state=ActorTableData.ActorState.DEAD,
class_name=class_b,
),
generate_actor_data(
actor_ids[2].binary(),
state=ActorTableData.ActorState.PENDING_CREATION,
class_name=class_b,
),
generate_actor_data(
actor_ids[3].binary(),
state=ActorTableData.ActorState.DEPENDENCIES_UNREADY,
class_name=class_b,
),
generate_actor_data(
actor_ids[4].binary(),
state=ActorTableData.ActorState.RESTARTING,
class_name=class_b,
),
generate_actor_data(
actor_ids[5].binary(),
state=ActorTableData.ActorState.RESTARTING,
class_name=class_b,
),
]
)
result = await state_api_manager.summarize_actors(option=create_summary_options())
data = result.result
assert "cluster" in result.result.node_id_to_summary
data = result.result.node_id_to_summary["cluster"]
assert data.total_actors == 6
assert data.summary[class_a].class_name == class_a
assert data.summary[class_a].state_counts["ALIVE"] == 1
assert data.summary[class_b].class_name == class_b
assert data.summary[class_b].state_counts["DEAD"] == 1
assert data.summary[class_b].state_counts["DEPENDENCIES_UNREADY"] == 1
assert data.summary[class_b].state_counts["PENDING_CREATION"] == 1
assert data.summary[class_b].state_counts["RESTARTING"] == 2
"""
Test if it can be correctly modified to a dictionary.
"""
print(result.result)
result_in_dict = asdict(result.result)
assert json.loads(json.dumps(result_in_dict)) == result_in_dict
@pytest.mark.asyncio
async def test_api_manager_summary_objects(state_api_manager):
data_source_client = state_api_manager.data_source_client
object_ids = [ObjectID((f"{i}" * 28).encode()) for i in range(9)]
data_source_client.get_all_node_info = AsyncMock()
data_source_client.get_all_node_info.return_value = (
{
NodeID.from_binary(b"1" * 28): GcsNodeInfo(
node_id=b"1" * 28, state=GcsNodeInfo.GcsNodeState.ALIVE
),
NodeID.from_binary(b"2" * 28): GcsNodeInfo(
node_id=b"2" * 28, state=GcsNodeInfo.GcsNodeState.ALIVE
),
},
0,
)
first_callsite = "first.py"
second_callsite = "second.py"
data_source_client.get_object_info = AsyncMock()
data_source_client.get_object_info.side_effect = [
GetObjectsInfoReply(
core_workers_stats=[
generate_object_info(
object_ids[0].binary(),
size_bytes=1024**2, # 1MB,
callsite=first_callsite,
task_state=TaskStatus.PENDING_NODE_ASSIGNMENT,
local_ref_count=2,
attempt_number=0,
pid=1,
ip="123",
worker_type=WorkerType.WORKER,
pinned_in_memory=False,
),
generate_object_info(
object_ids[1].binary(),
size_bytes=1024**2, # 1MB,
callsite=first_callsite,
task_state=TaskStatus.PENDING_NODE_ASSIGNMENT,
local_ref_count=2,
pid=2,
ip="123",
worker_type=WorkerType.WORKER,
),
generate_object_info(
object_ids[2].binary(),
size_bytes=-1,
callsite=first_callsite,
task_state=TaskStatus.RUNNING,
local_ref_count=1,
attempt_number=0,
pid=3,
ip="1234",
worker_type=WorkerType.WORKER,
),
],
total=3,
),
GetObjectsInfoReply(
core_workers_stats=[
generate_object_info(
object_ids[3].binary(),
size_bytes=1024**2 * 2, # 2MB,
callsite=first_callsite,
task_state=TaskStatus.RUNNING,
local_ref_count=1,
attempt_number=0,
pid=1,
ip="1234",
worker_type=WorkerType.WORKER,
),
generate_object_info(
object_ids[4].binary(),
size_bytes=1024**2, # 1MB,
callsite=second_callsite,
task_state=TaskStatus.RUNNING,
local_ref_count=4,
pid=1,
attempt_number=0,
ip="1234",
worker_type=WorkerType.DRIVER,
),
],
total=2,
),
]
result = await state_api_manager.summarize_objects(option=create_summary_options())
assert "cluster" in result.result.node_id_to_summary
data = result.result.node_id_to_summary["cluster"]
assert data.total_objects == 5
assert data.total_size_mb == 5.0
summary = data.summary
first_summary = summary[first_callsite]
assert first_summary.total_objects == 4
assert first_summary.total_size_mb == 4.0
assert first_summary.total_num_workers == 3
assert first_summary.total_num_nodes == 2
assert first_summary.task_state_counts["PENDING_NODE_ASSIGNMENT"] == 2
assert first_summary.task_state_counts["RUNNING"] == 2
assert first_summary.task_attempt_number_counts["1"] == 3
assert first_summary.task_attempt_number_counts["2"] == 1
assert first_summary.ref_type_counts["PINNED_IN_MEMORY"] == 3
assert first_summary.ref_type_counts["USED_BY_PENDING_TASK"] == 1
second_summary = summary[second_callsite]
assert second_summary.total_objects == 1
assert second_summary.total_size_mb == 1.0
assert second_summary.total_num_workers == 1
assert second_summary.total_num_nodes == 1
assert second_summary.task_state_counts["RUNNING"] == 1
assert second_summary.task_attempt_number_counts["1"] == 1
assert second_summary.ref_type_counts["PINNED_IN_MEMORY"] == 1
"""
Test if it can be correctly modified to a dictionary.
"""
result_in_dict = asdict(result.result)
assert json.loads(json.dumps(result_in_dict)) == result_in_dict
@pytest.mark.parametrize(
"event_routing_config", ["default", "aggregator"], indirect=True
)
@pytest.mark.usefixtures("event_routing_config")
def test_task_summary(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node(num_cpus=2)
ray.init(address=cluster.address)
cluster.add_node(num_cpus=2)
# Wait for aggregator agents on all nodes
for node in ray.nodes():
wait_for_aggregator_agent_if_enabled(cluster.address, node["NodeID"])
@ray.remote
def run_long_time_task():
time.sleep(30)
return True
@ray.remote
def task_wait_for_dep(dep):
print(dep)
a = task_wait_for_dep.remote(run_long_time_task.remote()) # noqa
b = task_wait_for_dep.remote(run_long_time_task.remote()) # noqa
def verify():
# task_name -> states
task_summary = summarize_tasks()
task_summary = task_summary["cluster"]["summary"]
assert "task_wait_for_dep" in task_summary
assert "run_long_time_task" in task_summary
assert (
task_summary["task_wait_for_dep"]["state_counts"]["PENDING_ARGS_AVAIL"] == 2
)
assert task_summary["run_long_time_task"]["state_counts"]["RUNNING"] == 2
assert task_summary["task_wait_for_dep"]["type"] == "NORMAL_TASK"
return True
wait_for_condition(verify)
# Test custom task name
task_wait_for_dep.options(name="custom_task_name").remote(
run_long_time_task.remote()
)
def verify_custom_name():
task_summary = summarize_tasks()
task_summary = task_summary["cluster"]["summary"]
assert "custom_task_name" in task_summary
assert (
task_summary["custom_task_name"]["state_counts"]["PENDING_ARGS_AVAIL"] >= 1
)
return True
wait_for_condition(verify_custom_name)
"""
Test CLI
"""
runner = CliRunner()
result = runner.invoke(summary_state_cli_group, ["tasks"])
assert "task_wait_for_dep" in result.output
assert result.exit_code == 0
@pytest.mark.parametrize(
"event_routing_config", ["default", "aggregator"], indirect=True
)
@pytest.mark.usefixtures("event_routing_config")
def test_actor_summary(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node(num_cpus=2)
ray.init(address=cluster.address)
cluster.add_node(num_cpus=2)
@ray.remote(num_gpus=1)
class Infeasible:
pass
@ray.remote(num_cpus=2)
class Actor:
pass
infeasible = Infeasible.remote() # noqa
running = [Actor.remote() for _ in range(2)] # noqa
pending = Actor.remote() # noqa
def verify():
summary = summarize_actors()
summary = summary["cluster"]["summary"]
actor_summary = None
infeasible_summary = None
for actor_class_name, s in summary.items():
if ".Actor" in actor_class_name:
actor_summary = s
elif ".Infeasible" in actor_class_name:
infeasible_summary = s
assert actor_summary["state_counts"]["PENDING_CREATION"] == 1
assert actor_summary["state_counts"]["ALIVE"] == 2
assert infeasible_summary["state_counts"]["PENDING_CREATION"] == 1
return True
wait_for_condition(verify)
"""
Test CLI
"""
runner = CliRunner()
result = runner.invoke(summary_state_cli_group, ["actors"])
assert "Infeasible" in result.output
assert result.exit_code == 0
@pytest.mark.parametrize(
"event_routing_config", ["default", "aggregator"], indirect=True
)
@pytest.mark.usefixtures("event_routing_config")
def test_object_summary(monkeypatch, ray_start_cluster):
with monkeypatch.context() as m:
m.setenv("RAY_record_ref_creation_sites", "1")
cluster = ray_start_cluster
cluster.add_node(num_cpus=4)
ray.init(address=cluster.address)
dep = ray.put(1) # noqa
@ray.remote
def task_wait_for_dep(dep):
time.sleep(30)
a = [task_wait_for_dep.remote(dep) for _ in range(2)] # noqa
def verify():
summary = summarize_objects()
assert "cluster" in summary
assert summary["cluster"]["callsite_enabled"] is True
summary = summary["cluster"]["summary"]
deserialized_task_arg_summary = None
put_obj_summary = None
return_ref_summary = None
for k, v in summary.items():
if "(deserialize task arg)" in k:
deserialized_task_arg_summary = v
elif "(put object)" in k:
put_obj_summary = v
elif "(task call)" in k:
return_ref_summary = v
assert deserialized_task_arg_summary["total_objects"] == 2
assert deserialized_task_arg_summary["total_num_workers"] == 2
assert deserialized_task_arg_summary["total_num_nodes"] == 1
assert deserialized_task_arg_summary["task_state_counts"]["NIL"] == 2
assert (
deserialized_task_arg_summary["ref_type_counts"]["PINNED_IN_MEMORY"]
== 2
)
assert put_obj_summary["total_objects"] == 1
assert put_obj_summary["total_num_workers"] == 1
assert put_obj_summary["total_num_nodes"] == 1
assert put_obj_summary["task_state_counts"]["FINISHED"] == 1
assert put_obj_summary["ref_type_counts"]["USED_BY_PENDING_TASK"] == 1
assert return_ref_summary["total_objects"] == 2
assert return_ref_summary["total_num_workers"] == 1
assert return_ref_summary["total_num_nodes"] == 1
assert return_ref_summary["task_state_counts"]["SUBMITTED_TO_WORKER"] == 2
assert return_ref_summary["ref_type_counts"]["LOCAL_REFERENCE"] == 2
return True
wait_for_condition(verify)
"""
Test CLI
"""
runner = CliRunner()
result = runner.invoke(summary_state_cli_group, ["objects"])
assert "(deserialize task arg)" in result.output
assert result.exit_code == 0
def test_summarize_by_lineage():
"""
Unit test for summarize by lineage.
This test starts with an expected lineage.
It then converts that into a single list of tasks
It then randomizes the order of that list.
It calls the summarize_by_lineage_function with the randomized list.
Then asserts the final result should be the same.
"""
expected_summary = [
NestedTaskSummary(
name="TuneActor",
key="actor:tune-actor-0",
type="ACTOR",
timestamp=1000,
state_counts={
"FINISHED": 111,
"RUNNING": 10,
},
link=Link("actor", "tune-actor-0"),
children=[
NestedTaskSummary(
name="TuneActor.__init__",
key="tune-actor-init-0",
type="ACTOR_CREATION_TASK",
timestamp=1000,
state_counts={
"FINISHED": 111,
"RUNNING": 10,
},
link=Link("task", "tune-actor-init-0"),
children=[
NestedTaskSummary(
name="TrainActor",
key="TrainActor",
type="GROUP",
timestamp=1100,
state_counts={
"FINISHED": 110,
"RUNNING": 10,
},
children=[
NestedTaskSummary(
name="TrainActor",
key=f"actor:train-actor-{i}",
type="ACTOR",
timestamp=1100 + i,
state_counts={
"FINISHED": 11,
"RUNNING": 1,
},
link=Link("actor", f"train-actor-{i}"),
children=[
NestedTaskSummary(
name="TrainActor.train_step_reduce",
key=f"train-actor-train-step-reduce-{i}",
type="ACTOR_TASK",
timestamp=2200,
state_counts={
"RUNNING": 1,
},
link=Link(
"task",
f"train-actor-train-step-reduce-{i}",
),
),
NestedTaskSummary(
name="TrainActor.__init__",
key=f"train-actor-init-{i}",
type="ACTOR_CREATION_TASK",
timestamp=1100 + i,
state_counts={
"FINISHED": 1,
},
link=Link("task", f"train-actor-init-{i}"),
),
NestedTaskSummary(
name="TrainActor.train_step_map",
key="TrainActor.train_step_map",
type="GROUP",
timestamp=2100,
state_counts={
"FINISHED": 10,
},
children=[
NestedTaskSummary(
name="TrainActor.train_step_map",
key=(
"train-actor-train-step-map-"
f"{i}-{j}"
),
type="ACTOR_TASK",
timestamp=2100 + j,
state_counts={
"FINISHED": 1,
},
link=Link(
"task",
"train-actor-train-step-map-"
f"{i}-{j}",
),
)
for j in range(10)
],
),
],
)
for i in range(10)
],
)
],
)
],
),
NestedTaskSummary(
name="preprocess",
key="preprocess",
type="GROUP",
timestamp=100,
state_counts={
"FINISHED": 20,
},
children=[
NestedTaskSummary(
name="preprocess",
key=f"preprocess-{i}",
type="NORMAL_TASK",
timestamp=100 + i,
state_counts={
"FINISHED": 2,
},
link=Link("task", f"preprocess-{i}"),
children=[
NestedTaskSummary(
name="preprocess_sub_task",
key=f"preprocess-{i}-0",
type="NORMAL_TASK",
timestamp=200,
state_counts={
"FINISHED": 1,
},
link=Link("task", f"preprocess-{i}-0"),
)
],
)
for i in range(10)
],
),
]
tasks = []
def grab_tasks_from_task_group(
task_group: NestedTaskSummary, actor_id=None, parent_task_id=None
):
if task_group.type != "ACTOR" and task_group.type != "GROUP":
# "Virtual" groups don't have underlying tasks.
task = {
"name": task_group.name,
"task_id": task_group.key,
"parent_task_id": parent_task_id,
"state": "RUNNING"
if task_group.name == "TrainActor.train_step_reduce"
else "FINISHED",
"actor_id": actor_id,
"creation_time_ms": task_group.timestamp,
"func_or_class_name": task_group.name,
"type": task_group.type,
}
tasks.append(task)
actor_id_for_child = None
parent_task_id_for_child = None
if task_group.type == "ACTOR":
[_, actor_id_for_child] = task_group.key.split(":")
parent_task_id_for_child = parent_task_id
elif task_group.type == "GROUP":
actor_id_for_child = actor_id
parent_task_id_for_child = parent_task_id
else:
parent_task_id_for_child = task_group.key
for child in task_group.children:
grab_tasks_from_task_group(
child,
actor_id=actor_id_for_child,
parent_task_id=parent_task_id_for_child,
)
for group in expected_summary:
grab_tasks_from_task_group(group, None, f"{DRIVER_TASK_ID_PREFIX}01000000")
random.shuffle(tasks)
summary = TaskSummaries.to_summary_by_lineage(tasks=tasks, actors=[])
assert summary.total_tasks == 20
assert summary.total_actor_tasks == 110
assert summary.total_actor_scheduled == 11
assert summary.summary == expected_summary
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))