397 lines
11 KiB
Python
397 lines
11 KiB
Python
import sys
|
|
from collections import defaultdict
|
|
from typing import Dict
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import requests
|
|
|
|
import ray
|
|
from ray._common.test_utils import (
|
|
PrometheusTimeseries,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private.test_utils import raw_metric_timeseries
|
|
from ray._private.worker import RayContext
|
|
from ray.dashboard.consts import RAY_DASHBOARD_STATS_UPDATING_INTERVAL
|
|
|
|
KiB = 1 << 10
|
|
MiB = 1 << 20
|
|
|
|
_SYSTEM_CONFIG = {
|
|
"automatic_object_spilling_enabled": True,
|
|
"max_io_workers": 100,
|
|
"min_spilling_size": 1,
|
|
"object_spilling_threshold": 0.99, # to prevent premature spilling
|
|
"metrics_report_interval_ms": 200,
|
|
}
|
|
|
|
|
|
def _objects_by_tag(
|
|
info: RayContext, tag: str, timeseries: PrometheusTimeseries
|
|
) -> Dict:
|
|
res = raw_metric_timeseries(info, timeseries)
|
|
objects_info = defaultdict(int)
|
|
if "ray_object_store_memory" in res:
|
|
for sample in res["ray_object_store_memory"]:
|
|
# NOTE: SPILLED sample doesn't report sealing states. So need to
|
|
# filter those empty label value out.
|
|
print(sample)
|
|
if tag in sample.labels and sample.labels[tag] != "":
|
|
objects_info[sample.labels[tag]] += sample.value
|
|
|
|
print(f"Objects by {tag}: {objects_info}")
|
|
return objects_info
|
|
|
|
|
|
def objects_by_seal_state(info: RayContext, timeseries: PrometheusTimeseries) -> Dict:
|
|
return _objects_by_tag(info, "ObjectState", timeseries)
|
|
|
|
|
|
def objects_by_loc(info: RayContext, timeseries: PrometheusTimeseries) -> Dict:
|
|
return _objects_by_tag(info, "Location", timeseries)
|
|
|
|
|
|
def approx_eq_dict_in(actual: Dict, expected: Dict, e: int) -> bool:
|
|
"""Check if two dict are approximately similar (with error allowed)"""
|
|
assert set(actual.keys()) == set(expected.keys()), "Unequal key sets."
|
|
|
|
for k, actual_v in actual.items():
|
|
expect_v = expected[k]
|
|
assert (
|
|
abs(expect_v - actual_v) <= e
|
|
), f"expect={expect_v}, actual={actual_v}, diff allowed={e}"
|
|
|
|
return True
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "darwin", reason="Timing out on macos. Not enough time to run."
|
|
)
|
|
def test_shared_memory_and_inline_worker_heap(shutdown_only):
|
|
"""Test objects allocated in shared memory"""
|
|
info = ray.init(
|
|
object_store_memory=100 * MiB,
|
|
_system_config={
|
|
**_SYSTEM_CONFIG,
|
|
**{
|
|
"max_direct_call_object_size": 10 * MiB,
|
|
"task_rpc_inlined_bytes_limit": 100 * MiB,
|
|
},
|
|
},
|
|
)
|
|
timeseries = PrometheusTimeseries()
|
|
|
|
# Allocate 80MiB data
|
|
objs_in_use = ray.get(
|
|
[ray.put(np.zeros(20 * MiB, dtype=np.uint8)) for _ in range(4)]
|
|
)
|
|
|
|
expected = {
|
|
"MMAP_SHM": 80 * MiB,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 2 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
# Allocate inlined task returns
|
|
@ray.remote(num_cpus=0.1)
|
|
def func():
|
|
return np.zeros(4 * MiB, dtype=np.uint8)
|
|
|
|
tasks_with_inlined_return = [func.remote() for _ in range(5)]
|
|
|
|
expected = {
|
|
"MMAP_SHM": 80 * MiB,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 20 * MiB,
|
|
}
|
|
|
|
returns = ray.get(tasks_with_inlined_return)
|
|
|
|
wait_for_condition(
|
|
# 4 KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 4 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
# Free all of them
|
|
del objs_in_use
|
|
del returns
|
|
del tasks_with_inlined_return
|
|
|
|
expected = {
|
|
"MMAP_SHM": 0,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 2 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "darwin", reason="Timing out on macos. Not enough time to run."
|
|
)
|
|
def test_spilling(object_spilling_config, shutdown_only):
|
|
"""Test metrics with object spilling occurred"""
|
|
|
|
object_spilling_config, _ = object_spilling_config
|
|
delta = 5
|
|
info = ray.init(
|
|
num_cpus=1,
|
|
object_store_memory=100 * MiB + delta * MiB,
|
|
_system_config={
|
|
**_SYSTEM_CONFIG,
|
|
**{"object_spilling_config": object_spilling_config},
|
|
},
|
|
)
|
|
timeseries = PrometheusTimeseries()
|
|
# Create and use 100MiB data, which should fit in memory
|
|
objs1 = [ray.put(np.zeros(50 * MiB, dtype=np.uint8)) for _ in range(2)]
|
|
|
|
expected = {
|
|
"MMAP_SHM": 100 * MiB,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 1 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
# Create additional 100MiB, so that it needs to be triggered
|
|
objs2 = [ray.put(np.zeros(50 * MiB, dtype=np.uint8)) for _ in range(2)]
|
|
|
|
expected = {
|
|
"WORKER_HEAP": 0,
|
|
"MMAP_SHM": 100 * MiB,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 100 * MiB,
|
|
}
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 1 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
# Delete spilled objects
|
|
del objs1
|
|
expected = {
|
|
"MMAP_SHM": 100 * MiB,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 1 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
# Delete all
|
|
del objs2
|
|
expected = {
|
|
"MMAP_SHM": 0,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 1 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "darwin", reason="Timing out on macos. Not enough time to run."
|
|
)
|
|
def test_fallback_memory(shutdown_only):
|
|
"""Test some fallback allocated objects"""
|
|
|
|
expected_fallback = 6
|
|
expected_in_memory = 5
|
|
obj_size_mb = 20
|
|
|
|
# So expected_in_memory objects could fit in object store
|
|
delta_mb = 5
|
|
info = ray.init(
|
|
object_store_memory=expected_in_memory * obj_size_mb * MiB + delta_mb * MiB,
|
|
_system_config=_SYSTEM_CONFIG,
|
|
)
|
|
timeseries = PrometheusTimeseries()
|
|
obj_refs = [
|
|
ray.put(np.zeros(obj_size_mb * MiB, dtype=np.uint8))
|
|
for _ in range(expected_in_memory)
|
|
]
|
|
|
|
# Getting and using the objects to prevent spilling
|
|
in_use_objs = [ray.get(obj) for obj in obj_refs]
|
|
|
|
# No fallback and spilling yet
|
|
expected = {
|
|
"MMAP_SHM": expected_in_memory * obj_size_mb * MiB,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 2KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 3 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
# Fallback allocated and make them not spillable
|
|
obj_refs_fallback = []
|
|
in_use_objs_fallback = []
|
|
for _ in range(expected_fallback):
|
|
obj = ray.put(np.zeros(obj_size_mb * MiB, dtype=np.uint8))
|
|
in_use_objs_fallback.append(ray.get(obj))
|
|
obj_refs_fallback.append(obj)
|
|
|
|
# NOTE(rickyx): I actually wasn't aware this reference would
|
|
# keep the reference count? Removing this line would cause
|
|
# a single object not deleted.
|
|
del obj
|
|
|
|
# Fallback allocated and still no spilling
|
|
expected = {
|
|
"MMAP_SHM": expected_in_memory * obj_size_mb * MiB,
|
|
"MMAP_DISK": expected_fallback * obj_size_mb * MiB,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 3KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 3 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
# Free all of them
|
|
del in_use_objs
|
|
del obj_refs
|
|
del in_use_objs_fallback
|
|
del obj_refs_fallback
|
|
|
|
expected = {
|
|
"MMAP_SHM": 0,
|
|
"MMAP_DISK": 0,
|
|
"SPILLED": 0,
|
|
"WORKER_HEAP": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 3KiB for metadata difference
|
|
lambda: approx_eq_dict_in(objects_by_loc(info, timeseries), expected, 3 * KiB),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "darwin", reason="Timing out on macos. Not enough time to run."
|
|
)
|
|
def test_seal_memory(shutdown_only):
|
|
"""Test objects sealed states reported correctly"""
|
|
info = ray.init(
|
|
object_store_memory=100 * MiB,
|
|
_system_config=_SYSTEM_CONFIG,
|
|
)
|
|
timeseries = PrometheusTimeseries()
|
|
# Allocate 80MiB data
|
|
objs_in_use = ray.get(
|
|
[ray.put(np.zeros(20 * MiB, dtype=np.uint8)) for _ in range(4)]
|
|
)
|
|
|
|
expected = {
|
|
"SEALED": 80 * MiB,
|
|
"UNSEALED": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(
|
|
objects_by_seal_state(info, timeseries), expected, 2 * KiB
|
|
),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
del objs_in_use
|
|
|
|
expected = {
|
|
"SEALED": 0,
|
|
"UNSEALED": 0,
|
|
}
|
|
|
|
wait_for_condition(
|
|
# 1KiB for metadata difference
|
|
lambda: approx_eq_dict_in(
|
|
objects_by_seal_state(info, timeseries), expected, 2 * KiB
|
|
),
|
|
timeout=20,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
|
|
def test_object_store_memory_matches_dashboard_obj_memory(shutdown_only):
|
|
# https://github.com/ray-project/ray/issues/32092
|
|
# Verify the dashboard's object store memory report is same as
|
|
# the one from metrics
|
|
ctx = ray.init(
|
|
object_store_memory=500 * MiB,
|
|
)
|
|
timeseries = PrometheusTimeseries()
|
|
|
|
def verify():
|
|
resources = raw_metric_timeseries(ctx, timeseries)["ray_resources"]
|
|
object_store_memory_bytes_from_metrics = 0
|
|
for sample in resources:
|
|
# print(sample)
|
|
if sample.labels["Name"] == "object_store_memory":
|
|
object_store_memory_bytes_from_metrics += sample.value
|
|
|
|
r = requests.get(f"http://{ctx.dashboard_url}/nodes?view=summary")
|
|
object_store_memory_bytes_from_dashboard = int(
|
|
r.json()["data"]["summary"][0]["raylet"]["objectStoreAvailableMemory"]
|
|
)
|
|
|
|
assert (
|
|
object_store_memory_bytes_from_dashboard
|
|
== object_store_memory_bytes_from_metrics
|
|
)
|
|
assert object_store_memory_bytes_from_dashboard == 500 * MiB
|
|
return True
|
|
|
|
wait_for_condition(verify, timeout=RAY_DASHBOARD_STATS_UPDATING_INTERVAL * 1.5)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|