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
@@ -0,0 +1,43 @@
import sys
from ray.rllib.examples.multi_agent.utils.self_play_callback import SelfPlayCallback
from ray.rllib.examples.multi_agent.utils.self_play_callback_old_api_stack import (
SelfPlayCallbackOldAPIStack,
)
from ray.rllib.examples.multi_agent.utils.self_play_league_based_callback import (
SelfPlayLeagueBasedCallback,
)
from ray.rllib.examples.multi_agent.utils.self_play_league_based_callback_old_api_stack import ( # noqa
SelfPlayLeagueBasedCallbackOldAPIStack,
)
def ask_user_for_action(time_step):
"""Asks the user for a valid action on the command line and returns it.
Re-queries the user until she picks a valid one.
Args:
time_step: The open spiel Environment time-step object.
"""
pid = time_step.observations["current_player"]
legal_moves = time_step.observations["legal_actions"][pid]
choice = -1
while choice not in legal_moves:
print("Choose an action from {}:".format(legal_moves))
sys.stdout.flush()
choice_str = input()
try:
choice = int(choice_str)
except ValueError:
continue
return choice
__all__ = [
"ask_user_for_action",
"SelfPlayCallback",
"SelfPlayLeagueBasedCallback",
"SelfPlayCallbackOldAPIStack",
"SelfPlayLeagueBasedCallbackOldAPIStack",
]
@@ -0,0 +1,97 @@
from collections import defaultdict
import numpy as np
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
from ray.rllib.utils.metrics import ENV_RUNNER_RESULTS
class SelfPlayCallback(RLlibCallback):
def __init__(self, win_rate_threshold):
super().__init__()
# 0=RandomPolicy, 1=1st main policy snapshot,
# 2=2nd main policy snapshot, etc..
self.current_opponent = 0
self.win_rate_threshold = win_rate_threshold
# Report the matchup counters (who played against whom?).
self._matching_stats = defaultdict(int)
def on_episode_end(
self,
*,
episode,
env_runner,
metrics_logger,
env,
env_index,
rl_module,
**kwargs,
) -> None:
# Compute the win rate for this episode and log it with a window of 100.
main_agent = 0 if episode.module_for(0) == "main" else 1
rewards = episode.get_rewards()
if main_agent in rewards:
main_won = rewards[main_agent][-1] == 1.0
metrics_logger.log_value(
"win_rate",
main_won,
reduce="mean",
window=100,
)
def on_train_result(self, *, algorithm, metrics_logger=None, result, **kwargs):
win_rate = result[ENV_RUNNER_RESULTS]["win_rate"]
print(f"Iter={algorithm.iteration} win-rate={win_rate} -> ", end="")
# If win rate is good -> Snapshot current policy and play against
# it next, keeping the snapshot fixed and only improving the "main"
# policy.
if win_rate > self.win_rate_threshold:
self.current_opponent += 1
new_module_id = f"main_v{self.current_opponent}"
print(f"adding new opponent to the mix ({new_module_id}).")
# Re-define the mapping function, such that "main" is forced
# to play against any of the previously played modules
# (excluding "random").
def agent_to_module_mapping_fn(agent_id, episode, **kwargs):
# agent_id = [0|1] -> policy depends on episode ID
# This way, we make sure that both modules sometimes play
# (start player) and sometimes agent1 (player to move 2nd).
opponent = "main_v{}".format(
np.random.choice(list(range(1, self.current_opponent + 1)))
)
if hash(episode.id_) % 2 == agent_id:
self._matching_stats[("main", opponent)] += 1
return "main"
else:
return opponent
main_module = algorithm.get_module("main")
algorithm.add_module(
module_id=new_module_id,
module_spec=RLModuleSpec.from_module(main_module),
new_agent_to_module_mapping_fn=agent_to_module_mapping_fn,
)
# TODO (sven): Maybe we should move this convenience step back into
# `Algorithm.add_module()`? Would be less explicit, but also easier.
algorithm.set_state(
{
"learner_group": {
"learner": {
"rl_module": {
new_module_id: main_module.get_state(),
}
}
}
}
)
else:
print("not good enough; will keep learning ...")
# +2 = main + random
result["league_size"] = self.current_opponent + 2
print(f"Matchups:\n{self._matching_stats}")
@@ -0,0 +1,78 @@
import numpy as np
from ray._common.deprecation import Deprecated
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.utils.metrics import ENV_RUNNER_RESULTS
@Deprecated(help="Use the example for the new RLlib API stack.", error=False)
class SelfPlayCallbackOldAPIStack(RLlibCallback):
def __init__(self, win_rate_threshold):
super().__init__()
# 0=RandomPolicy, 1=1st main policy snapshot,
# 2=2nd main policy snapshot, etc..
self.current_opponent = 0
self.win_rate_threshold = win_rate_threshold
def on_train_result(self, *, algorithm, result, **kwargs):
# Get the win rate for the train batch.
# Note that normally, you should set up a proper evaluation config,
# such that evaluation always happens on the already updated policy,
# instead of on the already used train_batch.
main_rew = result[ENV_RUNNER_RESULTS]["hist_stats"].pop("policy_main_reward")
opponent_rew = list(result[ENV_RUNNER_RESULTS]["hist_stats"].values())[0]
assert len(main_rew) == len(opponent_rew)
won = 0
for r_main, r_opponent in zip(main_rew, opponent_rew):
if r_main > r_opponent:
won += 1
win_rate = won / len(main_rew)
result["win_rate"] = win_rate
print(f"Iter={algorithm.iteration} win-rate={win_rate} -> ", end="")
# If win rate is good -> Snapshot current policy and play against
# it next, keeping the snapshot fixed and only improving the "main"
# policy.
if win_rate > self.win_rate_threshold:
self.current_opponent += 1
new_pol_id = f"main_v{self.current_opponent}"
print(f"adding new opponent to the mix ({new_pol_id}).")
# Re-define the mapping function, such that "main" is forced
# to play against any of the previously played policies
# (excluding "random").
def policy_mapping_fn(agent_id, episode, worker, **kwargs):
# agent_id = [0|1] -> policy depends on episode ID
# This way, we make sure that both policies sometimes play
# (start player) and sometimes agent1 (player to move 2nd).
return (
"main"
if episode.episode_id % 2 == agent_id
else "main_v{}".format(
np.random.choice(list(range(1, self.current_opponent + 1)))
)
)
main_policy = algorithm.get_policy("main")
new_policy = algorithm.add_policy(
policy_id=new_pol_id,
policy_cls=type(main_policy),
policy_mapping_fn=policy_mapping_fn,
config=main_policy.config,
observation_space=main_policy.observation_space,
action_space=main_policy.action_space,
)
# Set the weights of the new policy to the main policy.
# We'll keep training the main policy, whereas `new_pol_id` will
# remain fixed.
main_state = main_policy.get_state()
new_policy.set_state(main_state)
# We need to sync the just copied local weights (from main policy)
# to all the remote workers as well.
algorithm.env_runner_group.sync_weights()
else:
print("not good enough; will keep learning ...")
# +2 = main + random
result["league_size"] = self.current_opponent + 2
@@ -0,0 +1,299 @@
import re
from collections import defaultdict
from pprint import pprint
import numpy as np
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
from ray.rllib.utils.metrics import ENV_RUNNER_RESULTS
class SelfPlayLeagueBasedCallback(RLlibCallback):
def __init__(self, win_rate_threshold):
super().__init__()
# All policies in the league.
self.main_policies = {"main", "main_0"}
self.main_exploiters = {"main_exploiter_0", "main_exploiter_1"}
self.league_exploiters = {"league_exploiter_0", "league_exploiter_1"}
# Set of currently trainable policies in the league.
self.trainable_policies = {"main"}
# Set of currently non-trainable (frozen) policies in the league.
self.non_trainable_policies = {
"main_0",
"league_exploiter_0",
"main_exploiter_0",
}
# The win-rate value reaching of which leads to a new module being added
# to the leage (frozen copy of main).
self.win_rate_threshold = win_rate_threshold
# Store the win rates for league overview printouts.
self.win_rates = {}
# Report the matchup counters (who played against whom?).
self._matching_stats = defaultdict(int)
def on_episode_end(
self,
*,
episode,
env_runner,
metrics_logger,
env,
env_index,
rl_module,
**kwargs,
) -> None:
num_learning_policies = (
episode.module_for(0) in env_runner.config.policies_to_train
) + (episode.module_for(1) in env_runner.config.policies_to_train)
# Make sure the mapping function doesn't match two non-trainables together.
# This would be a waste of EnvRunner resources.
# assert num_learning_policies > 0
# Ignore matches between two learning policies and don't count win-rates for
# these.
assert num_learning_policies > 0, (
f"agent=0 -> mod={episode.module_for(0)}; "
f"agent=1 -> mod={episode.module_for(1)}; "
f"EnvRunner.config.policies_to_train={env_runner.config.policies_to_train}"
)
if num_learning_policies == 1:
# Compute the win rate for this episode (only looking at non-trained
# opponents, such as random or frozen policies) and log it with some window.
rewards_dict = episode.get_rewards()
for aid, rewards in rewards_dict.items():
mid = episode.module_for(aid)
won = rewards[-1] == 1.0
metrics_logger.log_value(
f"win_rate_{mid}",
won,
window=100,
)
def on_train_result(self, *, algorithm, metrics_logger=None, result, **kwargs):
local_worker = algorithm.env_runner
# Avoid `self` being pickled into the remote function below.
_trainable_policies = self.trainable_policies
# Get the win rate for the train batch.
# Note that normally, one should set up a proper evaluation config,
# such that evaluation always happens on the already updated policy,
# instead of on the already used train_batch.
league_changed = False
keys = [
k for k in result[ENV_RUNNER_RESULTS].keys() if k.startswith("win_rate_")
]
for key in keys:
module_id = key[9:]
self.win_rates[module_id] = result[ENV_RUNNER_RESULTS][key]
# Policy is frozen; ignore.
if module_id in self.non_trainable_policies:
continue
print(
f"Iter={algorithm.iteration} {module_id}'s "
f"win-rate={self.win_rates[module_id]} -> ",
end="",
)
# If win rate is good -> Snapshot current policy and decide,
# whether to freeze the copy or not.
if self.win_rates[module_id] > self.win_rate_threshold:
is_main = re.match("^main(_\\d+)?$", module_id)
initializing_exploiters = False
# First time, main manages a decent win-rate against random:
# Add league_exploiter_1 and main_exploiter_1 as trainables to the mix.
if is_main and len(self.trainable_policies) == 1:
initializing_exploiters = True
self.trainable_policies.add("league_exploiter_1")
self.trainable_policies.add("main_exploiter_1")
# If main manages to win (above threshold) against the entire league
# -> increase the league by another frozen copy of main,
# main-exploiters or league-exploiters.
else:
keep_training = (
False
if is_main
else np.random.choice([True, False], p=[0.3, 0.7])
)
if module_id in self.main_policies:
new_mod_id = re.sub(
"(main)(_\\d+)?$",
f"\\1_{len(self.main_policies) - 1}",
module_id,
)
self.main_policies.add(new_mod_id)
elif module_id in self.main_exploiters:
new_mod_id = re.sub(
"_\\d+$", f"_{len(self.main_exploiters)}", module_id
)
self.main_exploiters.add(new_mod_id)
else:
new_mod_id = re.sub(
"_\\d+$", f"_{len(self.league_exploiters)}", module_id
)
self.league_exploiters.add(new_mod_id)
if keep_training:
self.trainable_policies.add(new_mod_id)
else:
self.non_trainable_policies.add(new_mod_id)
print(f"adding new opponents to the mix ({new_mod_id}).")
# Initialize state variablers for agent-to-module mapping. Note, we
# need to keep track of the league-exploiter to always match a
# non-trainable policy with a trainable one - otherwise matches are
# a waste of resources.
self.type_count = 0
self.exploiter = None
def agent_to_module_mapping_fn(agent_id, episode, **kwargs):
# Pick whether this is ...
type_ = np.random.choice([1, 2])
# Each second third call reset state variables. Note, there will
# be always two agents playing against each others.
if self.type_count >= 2:
# Reset the counter.
self.type_count = 0
# Set the exploiter to `None`.
self.exploiter = None
# Increment the counter for each agent.
self.type_count += 1
# 1) League exploiter vs any other.
if type_ == 1:
# Note, the exploiter could be either of `type_==1` or `type_==2`.
if not self.exploiter:
self.exploiter = "league_exploiter_" + str(
np.random.choice(
list(range(len(self.league_exploiters)))
)
)
# This league exploiter is frozen: Play against a
# trainable policy.
if self.exploiter not in self.trainable_policies:
opponent = np.random.choice(list(self.trainable_policies))
# League exploiter is trainable: Play against any other
# non-trainable policy.
else:
opponent = np.random.choice(
list(self.non_trainable_policies)
)
# Only record match stats once per match.
if hash(episode.id_) % 2 == agent_id:
self._matching_stats[(self.exploiter, opponent)] += 1
return self.exploiter
else:
return opponent
# 2) Main exploiter vs main.
else:
# Note, the exploiter could be either of `type_==1` or `type_==2`.
if not self.exploiter:
self.exploiter = "main_exploiter_" + str(
np.random.choice(list(range(len(self.main_exploiters))))
)
# Main exploiter is frozen: Play against the main
# policy.
if self.exploiter not in self.trainable_policies:
main = "main"
# Main exploiter is trainable: Play against any
# frozen main.
else:
main = np.random.choice(list(self.main_policies - {"main"}))
# Only record match stats once per match.
if hash(episode.id_) % 2 == agent_id:
self._matching_stats[(self.exploiter, main)] += 1
return self.exploiter
else:
return main
multi_rl_module = local_worker.module
main_module = multi_rl_module["main"]
# Set the weights of the new polic(y/ies).
if initializing_exploiters:
main_state = main_module.get_state()
multi_rl_module["main_0"].set_state(main_state)
multi_rl_module["league_exploiter_1"].set_state(main_state)
multi_rl_module["main_exploiter_1"].set_state(main_state)
# We need to sync the just copied local weights to all the
# remote workers and remote Learner workers as well.
algorithm.env_runner_group.sync_weights(
policies=["main_0", "league_exploiter_1", "main_exploiter_1"]
)
algorithm.learner_group.set_weights(multi_rl_module.get_state())
else:
algorithm.add_module(
module_id=new_mod_id,
module_spec=RLModuleSpec.from_module(main_module),
)
# TODO (sven): Maybe we should move this convenience step back into
# `Algorithm.add_module()`? Would be less explicit, but also
# easier.
algorithm.set_state(
{
"learner_group": {
"learner": {
"rl_module": {
new_mod_id: multi_rl_module[
module_id
].get_state(),
}
}
}
}
)
algorithm.env_runner_group.foreach_env_runner(
lambda env_runner: env_runner.config.multi_agent(
policy_mapping_fn=agent_to_module_mapping_fn,
# This setting doesn't really matter for EnvRunners (no
# training going on there, but we'll update this as well
# here for good measure).
policies_to_train=_trainable_policies,
),
local_env_runner=True,
)
# Set all Learner workers' should_module_be_updated to the new
# value.
algorithm.learner_group.foreach_learner(
func=lambda learner: learner.config.multi_agent(
policies_to_train=_trainable_policies,
),
timeout_seconds=0.0, # fire-and-forget
)
league_changed = True
else:
print("not good enough; will keep learning ...")
# Add current league size to results dict.
result["league_size"] = len(self.non_trainable_policies) + len(
self.trainable_policies
)
if league_changed:
self._print_league()
def _print_league(self):
print("--- League ---")
print("Matchups:")
pprint(self._matching_stats)
print("Trainable policies (win-rates):")
for p in sorted(self.trainable_policies):
wr = self.win_rates[p] if p in self.win_rates else 0.0
print(f"\t{p}: {wr}")
print("Frozen policies:")
for p in sorted(self.non_trainable_policies):
wr = self.win_rates[p] if p in self.win_rates else 0.0
print(f"\t{p}: {wr}")
print()
@@ -0,0 +1,201 @@
import re
import numpy as np
from ray._common.deprecation import Deprecated
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.utils.metrics import ENV_RUNNER_RESULTS
@Deprecated(help="Use the example for the new RLlib API stack", error=False)
class SelfPlayLeagueBasedCallbackOldAPIStack(RLlibCallback):
def __init__(self, win_rate_threshold):
super().__init__()
# All policies in the league.
self.main_policies = {"main", "main_0"}
self.main_exploiters = {"main_exploiter_0", "main_exploiter_1"}
self.league_exploiters = {"league_exploiter_0", "league_exploiter_1"}
# Set of currently trainable policies in the league.
self.trainable_policies = {"main"}
# Set of currently non-trainable (frozen) policies in the league.
self.non_trainable_policies = {
"main_0",
"league_exploiter_0",
"main_exploiter_0",
}
# The win-rate value reaching of which leads to a new module being added
# to the leage (frozen copy of main).
self.win_rate_threshold = win_rate_threshold
# Store the win rates for league overview printouts.
self.win_rates = {}
def on_train_result(self, *, algorithm, result, **kwargs):
# Avoid `self` being pickled into the remote function below.
_trainable_policies = self.trainable_policies
# Get the win rate for the train batch.
# Note that normally, you should set up a proper evaluation config,
# such that evaluation always happens on the already updated policy,
# instead of on the already used train_batch.
for policy_id, rew in result[ENV_RUNNER_RESULTS]["hist_stats"].items():
mo = re.match("^policy_(.+)_reward$", policy_id)
if mo is None:
continue
policy_id = mo.group(1)
# Calculate this policy's win rate.
won = 0
for r in rew:
if r > 0.0: # win = 1.0; loss = -1.0
won += 1
win_rate = won / len(rew)
self.win_rates[policy_id] = win_rate
# Policy is frozen; ignore.
if policy_id in self.non_trainable_policies:
continue
print(
f"Iter={algorithm.iteration} {policy_id}'s " f"win-rate={win_rate} -> ",
end="",
)
# If win rate is good -> Snapshot current policy and decide,
# whether to freeze the copy or not.
if win_rate > self.win_rate_threshold:
is_main = re.match("^main(_\\d+)?$", policy_id)
initializing_exploiters = False
# First time, main manages a decent win-rate against random:
# Add league_exploiter_0 and main_exploiter_0 to the mix.
if is_main and len(self.trainable_policies) == 1:
initializing_exploiters = True
self.trainable_policies.add("league_exploiter_0")
self.trainable_policies.add("main_exploiter_0")
else:
keep_training = (
False
if is_main
else np.random.choice([True, False], p=[0.3, 0.7])
)
if policy_id in self.main_policies:
new_pol_id = re.sub(
"_\\d+$", f"_{len(self.main_policies) - 1}", policy_id
)
self.main_policies.add(new_pol_id)
elif policy_id in self.main_exploiters:
new_pol_id = re.sub(
"_\\d+$", f"_{len(self.main_exploiters)}", policy_id
)
self.main_exploiters.add(new_pol_id)
else:
new_pol_id = re.sub(
"_\\d+$", f"_{len(self.league_exploiters)}", policy_id
)
self.league_exploiters.add(new_pol_id)
if keep_training:
self.trainable_policies.add(new_pol_id)
else:
self.non_trainable_policies.add(new_pol_id)
print(f"adding new opponents to the mix ({new_pol_id}).")
# Update our mapping function accordingly.
def policy_mapping_fn(agent_id, episode, worker=None, **kwargs):
# Pick, whether this is ...
type_ = np.random.choice([1, 2])
# 1) League exploiter vs any other.
if type_ == 1:
league_exploiter = "league_exploiter_" + str(
np.random.choice(list(range(len(self.league_exploiters))))
)
# This league exploiter is frozen: Play against a
# trainable policy.
if league_exploiter not in self.trainable_policies:
opponent = np.random.choice(list(self.trainable_policies))
# League exploiter is trainable: Play against any other
# non-trainable policy.
else:
opponent = np.random.choice(
list(self.non_trainable_policies)
)
print(f"{league_exploiter} vs {opponent}")
return (
league_exploiter
if episode.episode_id % 2 == agent_id
else opponent
)
# 2) Main exploiter vs main.
else:
main_exploiter = "main_exploiter_" + str(
np.random.choice(list(range(len(self.main_exploiters))))
)
# Main exploiter is frozen: Play against the main
# policy.
if main_exploiter not in self.trainable_policies:
main = "main"
# Main exploiter is trainable: Play against any
# frozen main.
else:
main = np.random.choice(list(self.main_policies - {"main"}))
# print(f"{main_exploiter} vs {main}")
return (
main_exploiter
if episode.episode_id % 2 == agent_id
else main
)
# Set the weights of the new polic(y/ies).
if initializing_exploiters:
main_state = algorithm.get_policy("main").get_state()
pol_map = algorithm.env_runner.policy_map
pol_map["main_0"].set_state(main_state)
pol_map["league_exploiter_1"].set_state(main_state)
pol_map["main_exploiter_1"].set_state(main_state)
# We need to sync the just copied local weights to all the
# remote workers as well.
algorithm.env_runner_group.sync_weights(
policies=["main_0", "league_exploiter_1", "main_exploiter_1"]
)
def _set(worker):
worker.set_policy_mapping_fn(policy_mapping_fn)
worker.set_is_policy_to_train(_trainable_policies)
algorithm.env_runner_group.foreach_env_runner(_set)
else:
base_pol = algorithm.get_policy(policy_id)
new_policy = algorithm.add_policy(
policy_id=new_pol_id,
policy_cls=type(base_pol),
policy_mapping_fn=policy_mapping_fn,
policies_to_train=self.trainable_policies,
config=base_pol.config,
observation_space=base_pol.observation_space,
action_space=base_pol.action_space,
)
main_state = base_pol.get_state()
new_policy.set_state(main_state)
# We need to sync the just copied local weights to all the
# remote workers as well.
algorithm.env_runner_group.sync_weights(policies=[new_pol_id])
self._print_league()
else:
print("not good enough; will keep learning ...")
def _print_league(self):
print("--- League ---")
print("Trainable policies (win-rates):")
for p in sorted(self.trainable_policies):
wr = self.win_rates[p] if p in self.win_rates else 0.0
print(f"\t{p}: {wr}")
print("Frozen policies:")
for p in sorted(self.non_trainable_policies):
wr = self.win_rates[p] if p in self.win_rates else 0.0
print(f"\t{p}: {wr}")
print()