Files
ray-project--ray/python/ray/tune/tests/test_placeholder.py
T
2026-07-13 13:17:40 +08:00

317 lines
10 KiB
Python

import unittest
import numpy as np
from ray import tune
from ray.tune.impl.placeholder import (
_FunctionResolver,
_RefResolver,
create_resolvers_map,
inject_placeholders,
resolve_placeholders,
)
from ray.tune.search.sample import Float, Integer
class Dummy:
def __init__(self, value):
self.value = value
class PlaceholderTest(unittest.TestCase):
def testNotReplaced(self):
config = {
"param1": "ok",
"param2": ["not ok", tune.grid_search(["ok", "not ok"])],
"param3": {
"param4": tune.choice(["ok", "not ok"]),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
# Primitive typed choices are not replaced.
self.assertEqual(config["param2"][1]["grid_search"], ["ok", "not ok"])
self.assertEqual(config["param3"]["param4"].categories, ["ok", "not ok"])
def testGridSearch(self):
config = {
"param1": "ok",
"param2": ["not ok", tune.grid_search(["ok", Dummy("not ok")])],
"param3": {
"param4": tune.grid_search([Dummy("ok"), "not ok"]),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
self.assertEqual(
config["param2"][1]["grid_search"],
["ok", (_RefResolver.TOKEN, "1870fa9b")],
)
self.assertEqual(
config["param3"]["param4"]["grid_search"],
[(_RefResolver.TOKEN, "8515e998"), "not ok"],
)
# Pretend we picked a choice from the grid searches.
config["param2"][1] = (_RefResolver.TOKEN, "1870fa9b")
config["param3"]["param4"] = "not ok"
resolve_placeholders(config, replaced)
self.assertEqual(config["param2"][1].value, "not ok")
self.assertEqual(config["param3"]["param4"], "not ok")
def testCategorical(self):
config = {
"param1": "ok",
"param2": ["not ok", tune.choice([Dummy("ok"), "not ok"])],
"param3": {
"param4": tune.choice([Dummy("ok"), "not ok"]),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
self.assertEqual(
config["param2"][1].categories,
[(_RefResolver.TOKEN, "ec0e030c"), "not ok"],
)
self.assertEqual(
config["param3"]["param4"].categories,
[(_RefResolver.TOKEN, "8515e998"), "not ok"],
)
# Pretend we picked a choice from the categoricals.
config["param2"][1] = (_RefResolver.TOKEN, "ec0e030c")
config["param3"]["param4"] = "not ok"
resolve_placeholders(config, replaced)
self.assertEqual(config["param2"][1].value, "ok")
self.assertEqual(config["param3"]["param4"], "not ok")
def _testNonSearchSpaceRef(self, value):
"""Tests that non-primitives (numpy, lambda fn) get replaced by a reference."""
config = {"param": tune.choice([value, "other", value])}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
self.assertEqual(
config["param"].categories,
[
(_RefResolver.TOKEN, "ec15c422"),
"other",
(_RefResolver.TOKEN, "3c7edff5"),
],
)
def testNumpyToRef(self):
self._testNonSearchSpaceRef(np.arange(10))
def testLambdaToRef(self):
self._testNonSearchSpaceRef(lambda x: x)
def testFunction(self):
config = {
"param1": "ok",
"param2": ["not ok", tune.sample_from(lambda: "not ok")],
# Both lambdas, either taking spec or config, should work.
"param3": {
"param4": tune.sample_from(lambda spec: spec["config"]["param1"]),
},
"param4": {
"param4": tune.sample_from(lambda config: config["param1"]),
},
# Make sure dot notation also works with spec passed in.
"param5": {
"param4": tune.sample_from(lambda spec: spec.config["param1"]),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
self.assertEqual(config["param2"][1][0], _FunctionResolver.TOKEN)
self.assertEqual(config["param3"]["param4"][0], _FunctionResolver.TOKEN)
self.assertEqual(config["param4"]["param4"][0], _FunctionResolver.TOKEN)
self.assertEqual(config["param5"]["param4"][0], _FunctionResolver.TOKEN)
resolve_placeholders(config, replaced)
self.assertEqual(config["param2"][1], "not ok")
self.assertEqual(config["param3"]["param4"], "ok")
self.assertEqual(config["param4"]["param4"], "ok")
self.assertEqual(config["param5"]["param4"], "ok")
def testRefValue(self):
config = {
"param1": "ok",
"param2": ["not ok", Dummy("ok")],
"param3": {
"param4": Dummy("not ok"),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
self.assertEqual(config["param2"][1][0], _RefResolver.TOKEN)
self.assertEqual(config["param3"]["param4"][0], _RefResolver.TOKEN)
resolve_placeholders(config, replaced)
self.assertEqual(config["param2"][1].value, "ok")
self.assertEqual(config["param3"]["param4"].value, "not ok")
def testTuple(self):
class Dummy:
def __init__(self, value):
self.value = value
config = {
"param1": ("ok", "not ok"),
"param2": ["not ok", (1, Dummy("ok"))],
"param3": {
"param4": (1, [2, Dummy("not ok")], 3),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
self.assertTrue(isinstance(config["param1"], tuple))
self.assertEqual(config["param1"], ("ok", "not ok"))
self.assertTrue(isinstance(config["param2"][1], tuple))
self.assertTrue(isinstance(config["param3"]["param4"], tuple))
resolve_placeholders(config, replaced)
self.assertTrue(isinstance(config["param2"][1], tuple))
self.assertEqual(config["param2"][1][1].value, "ok")
self.assertTrue(isinstance(config["param3"]["param4"], tuple))
self.assertEqual(config["param3"]["param4"][1][1].value, "not ok")
def testOtherDomains(self):
config = {
"param1": tune.uniform(0, 1),
"param2": tune.randint(2, 3),
"param3": tune.qrandn(0, 1, 0.1),
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
# Normal params are not replaced.
self.assertTrue(isinstance(config["param1"], Float))
self.assertTrue(isinstance(config["param2"], Integer))
self.assertTrue(isinstance(config["param3"], Float))
def testPointToEval(self):
config = {
"param1": "ok",
"param2": ["not ok", tune.choice([Dummy("ok"), "not ok"])],
"param3": {
"param4": tune.sample_from(lambda spec: spec["config"]["param1"]),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
# Normal params are not replaced.
self.assertEqual(
config["param2"][1].categories,
[(_RefResolver.TOKEN, "ec0e030c"), "not ok"],
)
self.assertEqual(
config["param3"]["param4"], (_FunctionResolver.TOKEN, "134aff3a")
)
# Now, say we manually resolved the placeholders based on
# points_to_evaluate.
config["param2"][1] = "not_ok"
config["param3"]["param4"] = "ok"
resolve_placeholders(config, replaced)
# Params stays the same.
self.assertEqual(config["param2"][1], "not_ok")
self.assertEqual(config["param3"]["param4"], "ok")
def testSimpleNestedSearchSpaces(self):
config = {
"param1": "ok",
"param2": tune.choice(
[
tune.choice([Dummy(1), 2, 3]),
tune.uniform(5, 6),
]
),
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
# Manually resolve. Select the Dummy value.
config["param2"] = (_RefResolver.TOKEN, "6f33af83")
resolve_placeholders(config, replaced)
self.assertEqual(config["param2"].value, 1)
def testSimpleNestedSearchSpaces2(self):
config = {
"param1": "ok",
"param2": tune.choice(
[
(None, Dummy(1), None),
(Dummy(2), None, None),
(None, None, Dummy(3)),
]
),
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
# Manually resolve. Select the Dummy value.
config["param2"] = (None, None, (_RefResolver.TOKEN, "a1433750"))
resolve_placeholders(config, replaced)
self.assertEqual(config["param2"][2].value, 3)
def testResolveFunctionAfterRef(self):
config = {
"param1": "ok",
"param2": tune.choice([Dummy("ok"), "not ok"]),
"param3": {
"param4": tune.sample_from(lambda config: config["param2"]),
},
}
replaced = create_resolvers_map()
config = inject_placeholders(config, replaced)
# Manually resolve param2.
config["param2"] = (_RefResolver.TOKEN, "07cb6238")
resolve_placeholders(config, replaced)
# param3.param4 should get the same value as resolved param2.
self.assertEqual(config["param3"]["param4"].value, "ok")
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))