chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
@@ -0,0 +1,17 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .profiler import profiler # noqa: F401
__all__ = []
@@ -0,0 +1,241 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import logging
from abc import ABC, abstractmethod
from ..utils import get_logger, is_recompute_op
from .trial import (
OptimizationTunerTrial as Trial,
TrialStatus,
)
class AlgorithmBase(ABC):
"""
An Tuning algorithm is a class to find out an optimal configuration
given the selected tuning optimization pass(es) and the arguments to be tuned.
Different optimization pass(es) will correspond to a different algorithm,
where different search space **pruning rules** will applied.
In another word, the key "algorithm" for this class is the
search space pruning rules specific for the given optimization scenario.
"""
_REGISTERED_ALGORITHMS = {}
name = None
@staticmethod
def _register(algo_name, algo_class):
assert issubclass(algo_class, AlgorithmBase)
AlgorithmBase._REGISTERED_ALGORITHMS[algo_name] = algo_class
def __init__(self, config):
self._config = config
self._init_spaces()
self._logger = get_logger(logging.INFO)
self._changed_configs = []
@property
def changed_configs(self):
return self._changed_configs[:]
def collect_model_info(self, main_prog, startup_prog):
"""
Collect the model static info (from programs) that could be used to
pruning candidate trials and saving tuning time. For instance,
model info like number of model parameters and activation memory could be
used to prune candidate trial and decide the next trial.
"""
pass
@abstractmethod
def _init_spaces(self):
pass
@abstractmethod
def next_trial(self):
pass
@abstractmethod
def update(self, results):
"""
Update the algorithm with the results of last trial. Using this information is used to
pruning the search space of the future trial.
"""
pass
def get_config_from_trial(self, trial):
"""
Return a new fleet.DistributedStrategy with the configurations in trial.
"""
assert len(self._changed_configs) > 0
new_strategy = copy.deepcopy(self._config.dist_strategy)
for name in self._changed_configs:
config = getattr(trial.space, name)
setattr(new_strategy, name, config)
return new_strategy
def register_algor(name):
def impl(cls):
AlgorithmBase._register(name, cls)
cls.name = name
return cls
return impl
def new_algorithm(name, config):
algor_class = AlgorithmBase._REGISTERED_ALGORITHMS.get(name)
assert algor_class is not None, f"Algorithm {name} is not defined."
algor_obj = algor_class(config)
return algor_obj
@register_algor("sharding")
class ShardingStageAlgorithm(AlgorithmBase):
# TODO import trial class & copy strategy
def __init__(self, config):
super().__init__(config)
self._changed_configs = ["sharding"]
def _init_spaces(self):
self._max_stage = 3
self._trial_idx = 0
stage_range = self._config.sharding.get("tuning_range", None)
if stage_range:
assert set(stage_range).issubset({0, 1, 2, 3}), (
f"Sharding Stage should belong into range within 0 - 3 but got {stage_range}."
)
stage_range.sort(reverse=True)
else:
stage_range = list(range(self._max_stage + 1))
stage_range.sort(reverse=True)
self._stage_range = stage_range[:]
self._total_num_trial = len(self._stage_range)
def next_trial(self):
if self._trial_idx < self._total_num_trial:
stage = self._stage_range[self._trial_idx]
new_strategy = copy.deepcopy(self._config.dist_strategy)
sharding = new_strategy.sharding
sharding.stage = stage
name = f"trial-sharding-stage{stage}"
trial = Trial(new_strategy, name, self.changed_configs)
return trial
else:
return Trial(None, None, None, status=TrialStatus.STOPPED)
def update(self, results):
et = results.get("ErrorType", None)
if et and et == "ResourceExhaustedError":
self._trial_idx = self._total_num_trial
self._logger.info(
"Last trial is failed with OOM, all remaining trials are pruned to save time !"
)
else:
self._trial_idx += 1
@register_algor("recompute")
class RecomputeCheckpointAlgorithm(AlgorithmBase):
def __init__(self, config):
super().__init__(config)
self._changed_configs = ["recompute"]
def collect_model_info(self, main_prog, startup_prog):
segments = []
for op in main_prog.global_block().ops:
if not is_recompute_op(op):
continue
seg_name = op.attr('op_namescope')
if seg_name not in segments:
segments.append(seg_name)
self._total_num_trial = len(segments)
self._tuning_segments = list(range(len(segments)))
self._trial_left = 0
self._trial_right = len(segments) - 1
self._trial_idx = int(0 + (len(segments) - 1) / 2)
def _init_spaces(self):
self._recompute_mode = "all"
def next_trial(self):
if self._trial_idx < self._total_num_trial:
if self._recompute_mode == "all":
self._recompute_flag = False
new_strategy = copy.deepcopy(self._config.dist_strategy)
name = "trial-recompute-all-segments"
return Trial(new_strategy, name, self.changed_configs)
elif self._recompute_mode == "none":
self._recompute_flag = False
new_strategy = copy.deepcopy(self._config.dist_strategy)
recompute = new_strategy.recompute
recompute.enable = False
name = "trial-recompute-none-segments"
return Trial(new_strategy, name, self.changed_configs)
elif self._recompute_mode == "part":
new_no_recompute = self._tuning_segments[: self._trial_idx]
new_strategy = copy.deepcopy(self._config.dist_strategy)
recompute = new_strategy.recompute
recompute.no_recompute_segments.extend(new_no_recompute)
name = f"trial-recompute-part-segments-idx{self._trial_idx}"
return Trial(new_strategy, name, self.changed_configs)
else:
return Trial(None, None, None, status=TrialStatus.STOPPED)
def update(self, results):
et = results.get("ErrorType", None)
if self._recompute_mode == "all":
if et and et == "ResourceExhaustedError":
self._trial_idx = self._total_num_trial
self._logger.info(
"Recompute all candidate segments is failed with OOM, please reduce model size or batch size."
)
else:
self._recompute_mode = "none"
elif self._recompute_mode == "none":
if et and et == "ResourceExhaustedError":
self._recompute_mode = "part"
else:
self._trial_idx = self._total_num_trial
self._logger.info(
"Recompute is unnecessary for this model size, which will reduce the Throughput."
)
else:
if self._trail_left >= self._trail_right:
self._trial_idx = self._total_num_trial
elif et and et == "ResourceExhaustedError":
self._trail_left = self._trail_left
self._trail_right = self._trial_idx - 1
self._trial_idx = int(
self._trail_left
+ (self._trail_right - self._trail_left) / 2
)
else:
self._trail_left = self._trial_idx + 1
self._trail_right = self._trail_right
self._trial_idx = int(
self._trail_left
+ (self._trail_right - self._trail_left) / 2
)
@@ -0,0 +1,122 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import os
from ...strategy import Strategy
_tuning_supported_passes = ["sharding", "recompute"]
def _get_pass_config(strategy, pass_name):
config = getattr(strategy, pass_name)
return config
class TuningConfig:
"""
A uniform config wrap:
distributed strategy: the user defined configuration for optimization pass
tuning config: configuration for the tuning process: mode (profile or cost model), log dir, extra tuning config for optimization like search range for specific
"""
def __init__(self, strategy):
if not isinstance(strategy, Strategy):
raise TypeError("'strategy' must be object of class `Strategy`.")
self._tuning_passes_name = set()
self._dist_strategy = copy.deepcopy(strategy)
self._mode = None
self._profile_start_step = None
self._profile_end_step = None
self._project_dir = None
self._max_num_trial = None
self._early_stop = None
self._debug = None
self._initialize()
@property
def mode(self):
return self._mode
@property
def profile_start_step(self):
return self._profile_start_step
@property
def profile_end_step(self):
return self._profile_end_step
@property
def project_dir(self):
return self._project_dir
@property
def tuning_passes_name(self):
return self._tuning_passes_name
@property
def max_num_trial(self):
return self._max_num_trial
@property
def early_stop(self):
return self._early_stop
@property
def debug(self):
return self._debug
@property
def dist_strategy(self):
return self._dist_strategy
# initialize config with user define value or default value
def _initialize(self):
tuning_strategy = self._dist_strategy.tuning
self._mode = tuning_strategy.get("mode", "PROFILE")
self._profile_start_step = tuning_strategy.get("profile_start_step", 10)
self._profile_end_step = tuning_strategy.get("profile_end_step", 30)
self._max_num_trial = tuning_strategy.get("max_num_trial", 50)
self._early_stop = tuning_strategy.get("early_stop", None)
self._debug = tuning_strategy.get("debug", False)
project_dir = tuning_strategy.get("project_dir", None)
if not project_dir:
project_dir = os.path.join(os.getcwd(), "OptimizationTuning")
self._project_dir = project_dir
for p in _tuning_supported_passes:
if (
getattr(self._dist_strategy, p)
and _get_pass_config(self._dist_strategy, p).enable_tuning
):
# TODO distinguish different args of each passes
self._tuning_passes_name.add(p)
p_strategy = getattr(self._dist_strategy, p)
self.__dict__[p] = p_strategy
# # TODO verify the user defined configs
# tuning_config_for_pass = tuning_strategy.get(p, None)
# if tuning_config_for_pass:
# for k, v in tuning_config_for_pass.items():
# self.__dict__[p][k] = v
# (NOTE)tuning config ONLY wraps dist strategy for pass config which is to be tuned
def __getattr__(self, item):
return getattr(self._dist_strategy, item)
@@ -0,0 +1,643 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import json
import logging
# import yaml
import os
import pathlib
import pickle
import shlex
import shutil
import subprocess
import sys
import time
import paddle
from paddle.distributed.auto_parallel.static.completion import Completer
from paddle.distributed.auto_parallel.static.dist_context import (
DistributedContext,
)
from paddle.distributed.auto_parallel.static.partitioner import Partitioner
from paddle.distributed.auto_parallel.static.process_group import (
clear_all_process_groups,
get_all_process_groups,
new_process_group,
)
from paddle.distributed.auto_parallel.static.reshard import Resharder
from paddle.distributed.auto_parallel.static.utils import debug_program
from paddle.distributed.passes import PassContext, new_pass
from paddle.static import append_backward, program_guard
from ..utils import get_logger
from .algorithms import new_algorithm
from .config import TuningConfig
from .trial import TrialStatus
def _get_new_params_grads(target_program, ref_program, ref_params_grads):
ref_block = ref_program.global_block()
target_block = target_program.global_block()
target_params_grads = []
for p, g in ref_params_grads:
# NOTE grad var might not be generated
assert ref_block.has_var(p.name)
assert target_block.has_var(p.name)
new_p = target_block.var(p.name)
if g:
new_g = target_block.var(g.name)
else:
new_g = None
target_params_grads.append((new_p, new_g))
return target_params_grads
def _get_new_loss(target_program, ref_program, loss):
ref_block = ref_program.global_block()
target_block = target_program.global_block()
assert ref_block.has_var(loss.name)
return target_block.var(loss.name)
def parse_process_groups():
group_map = {}
all_process_groups = get_all_process_groups()
for process_group in all_process_groups:
group_map[process_group.id] = process_group.ranks
return group_map
def get_metric(results):
assert isinstance(results, dict), (
f"results should be type of dictionary, but got {type(results)}."
)
if 'Throughput' in results and isinstance(results['Throughput'], float):
return float(results['Throughput'])
else:
return -1.0
def parse_results(results):
if results['Throughput'] > 0:
return "Throughput: {} step / s.".format(results['Throughput'])
et = results.get("ErrorType", None)
if et == "ResourceExhaustedError":
return "Fail with OOM"
else:
return "Fail with UNKNOWN ERROR"
# TODO only dependent on dist context
# all env need to be start a new pass are member of dist context
def _copy_context(ref_dist_context):
# clear all process groups and recover the world process group
clear_all_process_groups()
ranks = []
for process_mesh in ref_dist_context._process_meshes:
ranks.extend(process_mesh.process_ids)
new_process_group(list(set(ranks)))
new_dist_context = DistributedContext()
new_dist_context._serial_main_program = (
ref_dist_context.serial_main_program.clone(for_test=False)
)
new_dist_context._serial_startup_program = (
ref_dist_context.serial_startup_program.clone(for_test=False)
)
# mapping variable into new dist context
if getattr(ref_dist_context, '_params_grads', None):
new_dist_context._params_grads = _get_new_params_grads(
new_dist_context.serial_main_program,
ref_dist_context.serial_main_program,
ref_dist_context._params_grads,
)
new_dist_context._serial_loss = _get_new_loss(
new_dist_context.serial_main_program,
ref_dist_context.serial_main_program,
ref_dist_context.serial_loss,
)
for key, var_list in ref_dist_context._serial_feed_vars.items():
new_var_list = []
for var in var_list:
block_idx = var.block.idx
var_name = var.name
var = new_dist_context._serial_main_program.blocks[
block_idx
]._var_recursive(var_name)
new_var_list.append(var)
new_dist_context._serial_feed_vars[key] = new_var_list
for key, var_list in ref_dist_context._serial_fetch_vars.items():
new_var_list = []
# metrics is a list of list
if key == "metrics":
for inner_var_list in var_list:
new_inner_var_list = []
for var in inner_var_list:
block_idx = var.block.idx
var_name = var.name
var = new_dist_context._serial_main_program.blocks[
block_idx
]._var_recursive(var_name)
new_inner_var_list.append(var)
new_var_list.append(new_inner_var_list)
else:
for var in var_list:
block_idx = var.block.idx
var_name = var.name
var = new_dist_context._serial_main_program.blocks[
block_idx
]._var_recursive(var_name)
new_var_list.append(var)
new_dist_context._serial_fetch_vars[key] = new_var_list
# copy information in forward and backward
new_dist_context._serial_optimizer = copy.deepcopy(
ref_dist_context.serial_optimizer
)
new_dist_context._dist_tensors_for_program = copy.deepcopy(
ref_dist_context._dist_tensors_for_program
)
new_dist_context._dist_ops_for_program = copy.deepcopy(
ref_dist_context._dist_ops_for_program
)
for pm in ref_dist_context.process_meshes:
new_dist_context.add_process_mesh(pm)
new_dist_context._dist_op_context = copy.deepcopy(
ref_dist_context._dist_op_context
)
new_dist_context._block_state = copy.deepcopy(ref_dist_context.block_state)
return new_dist_context
class OptimizationTuner:
"""
OptimizationTuner is used to manage the tuning procedure of hyper-parameters (configs)
of Optimization Pass in AutoParallel.
"""
def __init__(
self,
dist_context,
dataset,
inputs_spec,
labels_spec,
batch_size,
rank,
):
self._config = TuningConfig(dist_context.strategy)
# should not modify dist context from calling function
self._baseline_dist_context = _copy_context(dist_context)
self._baseline_completer = Completer(self._baseline_dist_context)
self._rank = rank
self._inputs_spec = inputs_spec
self._labels_spec = labels_spec
self._dataset = dataset
self._batch_size = batch_size
self._finished_trials = []
self._best_metric = None
self._best_iter = float("-inf")
self._logger = get_logger(logging.INFO)
self._build_programs_without_optimization()
self._select_tuning_algorithm()
@property
def project_dir(self):
dirname = self._config.project_dir
if not os.path.exists(dirname):
if self.rank == 0:
pathlib.Path(dirname).mkdir(parents=True, exist_ok=True)
return dirname
@property
def rank(self):
return self._rank
@property
def device_id(self):
return paddle.distributed.ParallelEnv().device_id
# TODO Generate complete program with all parts like forward, backward, update
# as well as parallelism transformation.
def _build_programs_without_optimization(self):
serial_main_program = self._baseline_dist_context.serial_main_program
serial_startup_program = (
self._baseline_dist_context.serial_startup_program
)
serial_loss = self._baseline_dist_context.serial_loss
with program_guard(serial_main_program, serial_startup_program):
params_grads = append_backward(
serial_loss,
distop_context=self._baseline_dist_context.dist_op_context,
)
self._baseline_completer.complete_backward_annotation(
serial_main_program
)
self._baseline_dist_context.block_state.parse_backward_blocks(
serial_main_program
)
self._baseline_dist_context._params_grads = params_grads
if self._config.debug:
baseline_dir = os.path.join(self.project_dir, "baseline")
if not os.path.exists(baseline_dir):
pathlib.Path(baseline_dir).mkdir(parents=True, exist_ok=True)
debug_program(
self._baseline_dist_context._serial_main_program,
baseline_dir,
"main",
)
debug_program(
self._baseline_dist_context._serial_startup_program,
baseline_dir,
"startup",
)
def _select_tuning_algorithm(self):
selected_passes_set = self._config.tuning_passes_name
algorithm_name = "_".join(sorted(selected_passes_set))
self._algorithm = new_algorithm(algorithm_name, self._config)
def _apply_optimization(self, trial):
new_strategy = trial.space
dist_context = _copy_context(self._baseline_dist_context)
pass_context = PassContext()
completer = Completer(dist_context)
main_program = dist_context.serial_main_program
startup_program = dist_context.serial_startup_program
# applying optimization pass
if new_strategy.amp.enable:
config = copy.deepcopy(new_strategy.amp.to_dict())
config["dist_context"] = dist_context
config["params_grads"] = dist_context._params_grads
# TODO AMP Pass should not use loss var
config["loss"] = dist_context.serial_loss
config["input_data"] = (
self._baseline_dist_context.serial_feed_vars["inputs"]
+ self._baseline_dist_context.serial_feed_vars["labels"]
)
if config["dtype"] == "float16" and config["level"] == "o2":
config["base_opt"] = dist_context.serial_optimizer
auto_parallel_fp16_pass = new_pass("auto_parallel_fp16", config)
auto_parallel_fp16_pass.apply(
[main_program], [startup_program], pass_context
)
dist_context._serial_loss = auto_parallel_fp16_pass.get_loss()
else:
auto_parallel_amp_pass = new_pass("auto_parallel_amp", config)
auto_parallel_amp_pass.apply(
[main_program], [startup_program], pass_context
)
dist_context._serial_loss = auto_parallel_amp_pass.get_loss()
if new_strategy.recompute.enable:
config = copy.deepcopy(new_strategy.recompute.to_dict())
config["dist_context"] = dist_context
config["no_grad_set"] = None
config["loss"] = dist_context.serial_loss
auto_parallel_recompute_pass = new_pass(
"auto_parallel_recompute", config
)
auto_parallel_recompute_pass.apply(
[main_program], [startup_program], pass_context
)
# Do logical partition
partitioner = Partitioner(dist_context, self.rank)
(
dist_main_prog,
dist_startup_prog,
dist_params_grads,
) = partitioner.partition(
main_program, startup_program, dist_context._params_grads
)
# Generate optimizer
# FIXME should be remove from apply pass after pass support optimizers
with (
program_guard(dist_main_prog, dist_startup_prog),
dist_main_prog.switch_name_generator_guard("opt_"),
):
optimizer_ops = dist_context.serial_optimizer.apply_gradients(
dist_params_grads
)
completer.complete_update_annotation(dist_main_prog)
resharder = Resharder(
dist_main_prog,
dist_startup_prog,
self.rank,
dist_context,
dist_params_grads,
)
resharder.reshard()
config = {}
config["dist_context"] = dist_context
config["global_rank"] = self.rank
config["use_sharding"] = new_strategy.sharding.enable
dp_pass = new_pass("auto_parallel_data_parallel_optimization", config)
dp_pass.apply([dist_main_prog], [dist_startup_prog], pass_context)
if new_strategy.sharding.enable:
config = copy.deepcopy(new_strategy.sharding.to_dict())
config["dist_context"] = dist_context
config["params_grads"] = dist_params_grads
config["global_rank"] = self.rank
auto_parallel_sharding_pass = new_pass(
"auto_parallel_sharding", config
)
auto_parallel_sharding_pass.apply(
[dist_main_prog], [dist_startup_prog], pass_context
)
dist_params_grads = pass_context.get_attr("params_grads")
# gradient clip
config = copy.deepcopy(new_strategy.sharding.to_dict())
config["dist_context"] = dist_context
config["params_grads"] = dist_params_grads
config["rank_id"] = self.rank
auto_parallel_clip_pass = new_pass("auto_parallel_grad_clip", config)
auto_parallel_clip_pass.apply(
[dist_main_prog], [dist_startup_prog], pass_context
)
if new_strategy.gradient_merge.enable:
config = copy.deepcopy(new_strategy.gradient_merge.to_dict())
config["dist_context"] = dist_context
config["params_grads"] = dist_params_grads
auto_parallel_gradient_merge_pass = new_pass(
"auto_parallel_gradient_merge_pass", config
)
auto_parallel_gradient_merge_pass.apply(
[dist_main_prog], [dist_startup_prog], pass_context
)
trial.main_program, trial.startup_program = (
dist_main_prog,
dist_startup_prog,
)
return trial
def _get_profile_context(self, trial, result_path):
profile_ctx = {}
profile_ctx['distributed_env'] = copy.deepcopy(
paddle.distributed.ParallelEnv()
)
profile_ctx['group_map'] = parse_process_groups()
profile_ctx["loss_var_name"] = (
self._baseline_dist_context.serial_loss.name
)
profile_ctx["main_program_decs"] = (
trial.main_program.desc.serialize_to_string()
)
profile_ctx["startup_program_decs"] = (
trial.startup_program.desc.serialize_to_string()
)
self._dataset.batch_size = self._batch_size
self._dataset.input_names = self._get_input_names()
profile_ctx["dataset"] = self._dataset
profile_ctx["result_filename"] = result_path
return profile_ctx
def _get_input_names(self):
input_names = []
for input_spec in self._inputs_spec[:] + self._labels_spec[:]:
input_names.append(input_spec.name)
return input_names
def _launch_profile(self, ctx_path, trial_dir):
if os.environ.get("WITH_COVERAGE", "OFF") == "ON":
coverage_args = ["-m", "coverage", "run", "--branch", "-p"]
else:
coverage_args = []
profile_args = " ".join(
[
"--rank",
str(self.rank),
"--device_id",
str(self.device_id),
"--ctx_filename",
ctx_path,
"--profile_start_step",
str(self._config.profile_start_step),
"--profile_end_step",
str(self._config.profile_end_step),
]
)
cmd_args = (
"-m paddle.distributed.auto_parallel.static.tuner.profiler"
+ " "
+ profile_args
)
cmd = [sys.executable, "-u", *coverage_args, *shlex.split(cmd_args)]
parent_env = copy.copy(os.environ.copy())
# env flags need for profile
new_env = {}
new_env.update(parent_env)
# TODO if any rank hang or fail, kill all processes
self._logger.debug("Executing cmd:\n{} .".format(" ".join(cmd)))
# new_process = subprocess.Popen(cmd, env=new_env)
with (
open(
os.path.join(trial_dir, "stdout.log" + str(self.rank)), "wb"
) as out,
open(
os.path.join(trial_dir, "stderr.log" + str(self.rank)), "wb"
) as err,
):
result = subprocess.Popen(cmd, stdout=out, stderr=err, env=new_env)
result.wait()
out.flush()
err.flush()
os.fsync(out)
os.fsync(err)
def _profile_trial(self, trial):
# Making working directory
trial_dir = self._get_trial_dir(trial)
if not os.path.exists(trial_dir):
if self.rank == 0:
pathlib.Path(trial_dir).mkdir(parents=True, exist_ok=True)
else:
while not os.path.exists(trial_dir):
pass
ctx_filename = "profile_ctx." + str(self.rank)
ctx_path = os.path.join(trial_dir, ctx_filename)
result_path = os.path.join(trial_dir, "result.json")
# Prepare Profile Context
profile_ctx = self._get_profile_context(trial, result_path)
with open(ctx_path, 'wb') as f:
pickle.dump(profile_ctx, f, protocol=4)
if self._config.debug:
debug_program(trial.main_program, trial_dir, "main_program")
debug_program(trial.startup_program, trial_dir, "startup_program")
# Run
self._launch_profile(ctx_path, trial_dir)
# Load results
try:
with open(result_path, 'r') as fp:
results = json.load(fp)
return results
except FileNotFoundError:
Error_results = {"Throughput": -1, "ErrorType": 'FatalError'}
return Error_results
def _evaluate_trial(self, trial):
self._logger.info(f"Trial {trial.name} evaluation start.")
self._apply_optimization(trial)
if self._config.mode == "PROFILE":
results = self._profile_trial(trial)
elif self._config.mode == "COSTMODEL":
raise NotImplementedError(
"COSTMODEL mode for optimization tuning is not supported yet!"
)
else:
raise NotImplementedError(
f"invalid evaluation mode: {self._config.mode}"
)
self._logger.info(
f"Trial {trial.name} evaluation finish with {parse_results(results)}."
)
return results
def _update(self, i, trial, results):
self._finished_trials.append(trial)
cur_metric = get_metric(results)
if self._best_metric is None or cur_metric > self._best_metric:
self._best_metric = cur_metric
self._best_iter = i
def _get_trial_dir(self, trial):
return os.path.join(self.project_dir, trial.name)
def get_best_config(self):
"""
Return the best optimization configuration found in the tuning.
Returns:
A object of fleet.DistributedStrategy with best configuration.
"""
assert self._best_iter >= 0, "The best configuration is not found yet !"
best_trial = self._finished_trials[self._best_iter]
return self._algorithm.get_config_from_trial(best_trial)
def summary(self):
"""
Display tuning result summary.
"""
# TODO summary with the trial_name with metric_of_trial
best_trial = self._finished_trials[self._best_iter]
summary_ = f"""
Tuning Result Summary
Run total {len(self._finished_trials)} trials with {(time.time() - self._tuning_start_time) / 60} min.
The best trial is: [{best_trial.name}], whose configuration is following:
"""
summary_ += "\n" + best_trial.summary() + "\n"
self._logger.info(summary_)
with open(os.path.join(self.project_dir, "summary.txt"), "w+") as fw:
fw.writelines(line + "\n" for line in summary_.split("\n"))
# full_strategy = self.get_best_config()
# path = os.path.join(self.project_dir, "tuned_dist_strategy.yaml")
# with open(path, 'w') as outfile:
# yaml.dump(full_strategy, outfile, default_flow_style=False)
def clear(self):
"""
Clear the temporary file generated in tuning procedure.
"""
# TODO clear up zombie process created by tuning
if not self._config.debug:
for trial in self._finished_trials:
trial_dir = self._get_trial_dir(trial)
shutil.rmtree(trial_dir, ignore_errors=True)
def tune(self):
"""
Performs the search for best hyperparameter configurations
for the selected optimization pass(es).
"""
# step1: collect model info which might be used for
# pruning the search space of the algorithm
self._tuning_start_time = time.time()
self._algorithm.collect_model_info(
self._baseline_dist_context.serial_main_program,
self._baseline_dist_context.serial_startup_program,
)
# main search loop
i = 0
while i < self._config.max_num_trial:
# step2: create a new trial
trial = self._algorithm.next_trial()
if trial.status == TrialStatus.STOPPED:
break
# step3: evaluate the trial
results = self._evaluate_trial(trial)
# step4: update the algorithm with last result,
# which could be used by algorithm to pruning the
# remaining search space.
self._algorithm.update(results)
self._update(i, trial, results)
# early stop
i += 1
if (
self._config.early_stop
and self._config.early_stop <= i - self._best_iter
):
self._logger.info(
f"Early stop the Tuning since there is no better trial found within [{self._config.early_stop}] trials"
)
break
# step5: summary the best config and return
self.summary()
self.clear()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,296 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import json
import os
import sys
import time
import traceback
import paddle
from paddle.distributed.auto_parallel.static.dist_loader import (
DistributedDataLoaderFromGenerator,
)
from paddle.distributed.auto_parallel.static.process_group import (
get_all_process_groups,
new_process_group,
)
from paddle.distributed.collective import _get_global_env
from paddle.framework import Program, _current_expected_place
from paddle.static import Operator
paddle.enable_static()
def str2bool(v):
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Unsupported value encountered.')
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--profile_start_step",
default=10,
type=int,
help="integer indicates the warmup step before starting profile.",
)
parser.add_argument(
"--profile_end_step",
default=30,
type=int,
help="integer indicates at the end step of profile.",
)
parser.add_argument(
"--rank",
type=int,
required=True,
help="the rank id of the this process.",
)
parser.add_argument(
"--device_id",
type=int,
required=True,
help="the device id of the this process.",
)
parser.add_argument(
"--ctx_filename",
type=str,
required=True,
help="the filename to the profile context file saved by optimization tuner",
)
args = parser.parse_args()
return args
def init_process_groups(group_map, rank):
for group_id, ranks in group_map.items():
if group_id == 0:
continue
new_process_group(ranks=ranks, group_id=group_id)
# TODO should instantiate global group first
all_process_groups = get_all_process_groups()
for process_group in all_process_groups:
print(process_group)
process_group.instantiate()
def get_cpp_error_type(error):
msg = str(error).splitlines()
cpp_error_types = [
'InvalidArgumentError',
'NotFoundError',
'OutOfRangeError',
'AlreadyExistsError',
'ResourceExhaustedError',
'PreconditionNotMetError',
'PermissionDeniedError',
'ExecutionTimeoutError',
'UnimplementedError',
'UnavailableError',
'FatalError',
'ExternalError',
]
error_type = 'FatalError'
for et in cpp_error_types:
for line in msg:
if et in line:
return et
return error_type
def create_dataloader(
main_program, startup_program, profile_ctx, epochs=1, steps_per_epoch=None
):
dataset = profile_ctx["dataset"]
main_block = main_program.global_block()
feed_list = []
for name in dataset.input_names:
if name in main_block.vars:
feed_list.append(main_block.vars[name])
# remove the first three ops if multi run fit/evaluate/predict
op_size = len(main_block.ops)
if main_block.ops[0].type == 'create_py_reader':
op_size -= 3
for _ in range(3):
main_block._remove_op(0, sync=False)
# insert read op at the end of program
places = paddle.static.cuda_places()
with paddle.static.program_guard(main_program, startup_program):
dataloader = DistributedDataLoaderFromGenerator(
dataset=dataset,
feed_list=feed_list,
capacity=70,
places=places,
batch_size=dataset.batch_size,
epochs=epochs,
steps_per_epoch=steps_per_epoch,
data_parallel_world_size=dataset.dp_world_size,
data_parallel_rank=dataset.dp_rank,
)
# move read op from the end of program to the start of program
new_op_size = len(main_block.ops)
for _ in range(new_op_size - 1, op_size - 1, -1):
op = main_block.ops[new_op_size - 1]
new_op_desc = main_block.desc._prepend_op()
new_op_desc.copy_from(op.desc)
new_op = Operator(main_block, new_op_desc, type=new_op_desc.type())
main_block.ops.insert(0, new_op)
for _ in range(new_op_size - op_size):
main_block._remove_op(new_op_size, sync=False)
main_block._sync_with_cpp()
return dataloader
def init_comm(profile_ctx):
# override the env for current process
dist_env = profile_ctx['distributed_env']
genv = _get_global_env()
genv = dist_env
print(
f"current process rank: {genv.rank}, device_id: {genv.device_id}, ip: {genv.current_endpoint}."
)
# init nccl comm
group_map = profile_ctx['group_map']
init_process_groups(group_map, args.rank)
def load_programs(profile_ctx):
main_program_desc_str = profile_ctx['main_program_decs']
main_program = Program.parse_from_string(main_program_desc_str)
startup_program_decs_str = profile_ctx['startup_program_decs']
startup_program = Program.parse_from_string(startup_program_decs_str)
loss_var_name = profile_ctx["loss_var_name"]
assert main_program.global_block().has_var(loss_var_name)
loss_var = main_program.global_block().var(loss_var_name)
return main_program, startup_program, loss_var
def get_executor():
place_type = _current_expected_place()
if not isinstance(place_type, paddle.CUDAPlace):
raise RuntimeError("OptimizationTuner only support CUDA GPU right now.")
genv = _get_global_env()
place = paddle.CUDAPlace(genv.device_id)
exe = paddle.static.Executor(place)
return exe
def profiler(args):
"""
main function to profile experiment for each pass hyper-parameter.
"""
# load ctx
if not os.path.isfile(args.ctx_filename):
raise ValueError(
f"There is no profile context named {args.ctx_filename}."
)
with open(args.ctx_filename, 'rb') as f:
from paddle.framework.restricted_unpickler import safe_load_pickle
profile_ctx = safe_load_pickle(f, encoding='latin1')
init_comm(profile_ctx)
main_program, startup_program, loss_var = load_programs(profile_ctx)
data_loader = create_dataloader(main_program, startup_program, profile_ctx)
result_path = profile_ctx["result_filename"]
exe = get_executor()
try:
exe.run(startup_program)
# profile main
duration = 0
eval_step = 0
data_loader._inner_dataloader.start()
while eval_step < args.profile_end_step:
start_time = time.time()
loss = exe.run(
main_program,
fetch_list=[loss_var],
use_program_cache=True,
)
end_time = time.time()
if eval_step >= args.profile_start_step:
duration += end_time - start_time
print(f"step: {eval_step}, loss_print: {loss[0]:f}")
eval_step += 1
avg_tput = (
1.0 * (args.profile_end_step - args.profile_start_step) / duration
)
result_dict = {
"Throughput": avg_tput,
"ErrorType": None,
}
if paddle.distributed.get_rank() == 0:
with open(result_path, 'w') as fp:
json.dump(result_dict, fp)
print(f"profile done! avg speed : {avg_tput} step / s.")
except paddle.framework.core.EOFException:
data_loader._inner_dataloader.reset()
except Exception as e:
error_type = get_cpp_error_type(e)
result_dict = {
"Throughput": -1,
"ErrorType": error_type,
}
if not os.path.isfile(result_path):
with open(result_path, 'w') as fp:
json.dump(result_dict, fp)
print(f"profile failed with error: [{error_type}]")
print(e)
print(traceback.format_exc())
data_loader._inner_dataloader.reset()
del data_loader._inner_dataloader
sys.exit(1)
data_loader._inner_dataloader.reset()
del data_loader._inner_dataloader
if __name__ == "__main__":
paddle.framework.set_flags({'FLAGS_new_executor_sequential_run': 1})
args = parse_args()
profiler(args)
@@ -0,0 +1,216 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Notice that the following codes are modified from KerasTuner for a different purpose.
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/metrics_tracking.py.
import numpy as np
class MetricRecord:
"""
One record for a single metric at a given execution step.
"""
def __init__(self, value, step):
self._value = value
self._step = step
@property
def value(self):
return self._value
@value.setter
def value(self, value):
self._value = value
@property
def step(self):
return self._step
@step.setter
def step(self, step):
self._step = step
def mean(self):
return np.mean(self.value)
def get_state(self):
return {"value": self.value, "step": self.step}
@classmethod
def from_state(cls, state):
return cls(**state)
def __eq__(self, other):
if not isinstance(other, MetricRecord):
return False
return other.value == self.value and other.step == self.step
def __repr__(self):
return f"MetricRecord(value={self.value}, step={self.step})"
class MetricRecords:
"""
Records of a single metric across different executions.
"""
def __init__(self, direction="min"):
if direction not in {"min", "max"}:
raise ValueError(
f"direction should be one of {{min, max}}, but got: {direction}."
)
self._direction = direction
self._records = {}
@property
def records(self):
return sorted(self._records.values(), key=lambda r: r.step)
@records.setter
def records(self, records):
for r in records:
self.update(r.value, step=r.step)
@property
def direction(self):
return self._direction
@direction.setter
def direction(self, direction):
self._direction = direction
def update(self, value, step=0):
if step in self._records:
self._records[step].set_value(value)
else:
self._records[step] = MetricRecord(value, step=step)
def get_best_value(self):
values = [r.mean() for r in self._records.values()]
if not values:
return None
if self._direction == "min":
return np.nanmin(values)
return np.nanmax(values)
def get_best_step(self):
best_value = self.get_best_value()
if best_value is None:
return None
for r in self._records.values():
if r.mean() == best_value:
return r.step
def get_statistics(self):
records = self.records
records_values = [r.mean() for r in records]
if not len(records_values):
return {}
return {
"min": float(np.nanmin(records_values)),
"max": float(np.nanmax(records_values)),
"mean": float(np.nanmean(records_values)),
"median": float(np.nanmedian(records_values)),
"var": float(np.nanvar(records_values)),
"std": float(np.nanstd(records_values)),
}
def get_state(self):
state = {}
state["direction"] = self._direction
state["records"] = [r.get_state() for r in self.records]
return state
@classmethod
def from_state(cls, state):
records = cls(state["direction"])
records.records = [MetricRecord.from_state(r) for r in state["records"]]
return records
class MetricsRecorder:
"""
Record the values for all metrics.
"""
def __init__(self, metrics=None):
self._records = {}
self.register_metrics(metrics)
@property
def records(self):
return self._records
def exists(self, name):
return name in self._records
def register_metrics(self, metrics=None):
metrics = metrics or []
for metric in metrics:
self.register(metric.name)
def register(self, name, direction=None):
if self.exists(name):
raise ValueError(f"Metric {name} have been registered.")
if direction is None:
direction = "min"
self._records[name] = MetricRecords(direction)
def update(self, name, value, step=0):
value = float(value)
if not self.exists(name):
self.register(name)
prev_best = self._records[name].get_best_value()
self._records[name].update(value, step=step)
new_best = self._records[name].get_best_value()
improved = new_best != prev_best
return improved
def get_records(self, name):
return self._records[name].records
def set_records(self, name, records):
if not self.exists(name):
self.register(name)
self._records[name].records = records
def get_best_value(self, name):
return self._records[name].get_best_value()
def get_best_step(self, name):
return self._records[name].get_best_step()
def get_statistics(self, name):
return self._records[name].get_statistics()
def get_state(self):
return {
"metrics": {
name: metric_records.get_state()
for name, metric_records in self._records.items()
}
}
@classmethod
def from_state(cls, state):
recorder = cls()
recorder._records = {
name: MetricRecords.from_state(metric_records)
for name, metric_records in state["metrics"].items()
}
return recorder
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Notice that the following codes are modified from KerasTuner for a different purpose.
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/metrics_tracking.py.
import json
class Storable:
def get_state(self):
raise NotImplementedError
def set_state(self, state):
raise NotImplementedError
def save(self, path):
state = self.get_state()
state_json = json.dumps(state)
try:
with open(path, "w") as f:
f.write(state_json)
return str(path)
except OSError as e:
raise OSError(f"Failed to save file at {path}: {e}") from e
def load(self, path):
with open(path, "r") as f:
state_data = f.read()
state = json.loads(state_data)
self.set_state(state)
@@ -0,0 +1,169 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Notice that the following codes are modified from KerasTuner to implement our own tuner.
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/trial.py.
import hashlib
import random
import time
from .recorder import MetricsRecorder
from .storable import Storable
from .tunable_space import TunableSpace
class TrialStatus:
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
STOPPED = "STOPPED"
INVALID = "INVALID"
class Trial(Storable):
def __init__(
self, tunable_space, trial_id=None, status=TrialStatus.RUNNING
):
self._id = _generate_trial_id() if trial_id is None else trial_id
self._space = tunable_space
self._recorder = MetricsRecorder()
self._score = None
self._best_step = None
self._status = status
@property
def id(self):
return self._id
@property
def space(self):
return self._space
@property
def recorder(self):
return self._recorder
@property
def score(self):
return self._score
@score.setter
def score(self, score):
self._score = score
@property
def best_step(self):
return self._best_step
@best_step.setter
def best_step(self, best_step):
self._best_step = best_step
@property
def status(self):
return self._status
@status.setter
def status(self, status):
self._status = status
def summary(self):
print("Tunable space:")
if self.space.values:
for tv, value in self.space.values.items():
print(tv + ":", value)
if self.score is not None:
print(f"Score: {self.score}")
def get_state(self):
return {
"id": self.id,
"space": self.space.get_state(),
"recorder": self.recorder.get_state(),
"score": self.score,
"best_step": self.best_step,
"status": self.status,
}
def set_state(self, state):
self._id = state["id"]
self._space = TunableSpace.from_state(state["space"])
self._recorder = MetricsRecorder.from_state(state["recorder"])
self._score = state["score"]
self._best_step = state["best_step"]
self._status = state["status"]
@classmethod
def from_state(cls, state):
trial = cls(tunable_space=None)
trial.set_state(state)
return trial
class OptimizationTunerTrial(Trial):
def __init__(
self,
config,
name,
changed_configs,
trial_id=None,
status=TrialStatus.RUNNING,
):
super().__init__(config, trial_id, status)
self._name = name
self._changed_configs = changed_configs
@property
def name(self):
return self._name
def summary(self):
spacing = 2
max_k = 38
max_v = 38
length = max_k + max_v + spacing
h1_format = " " + f"|{{:^{length}s}}|\n"
h2_format = " " + "|{{:>{}s}}{}{{:^{}s}}|\n".format(
max_k, " " * spacing, max_v
)
border = " +" + "".join(["="] * length) + "+"
line = " +" + "".join(["-"] * length) + "+"
draws = border + "\n"
draws += h1_format.format("")
draws += h1_format.format("Tuned Configurations Overview")
draws += h1_format.format("")
for name in self._changed_configs:
draws += border + "\n"
draws += h1_format.format(f"{name} auto=True <-> {name}")
draws += line + "\n"
my_configs = getattr(self.space, name)
keys = my_configs.to_dict().keys()
for key in keys:
draws += h2_format.format(
key, str(my_configs.to_dict().get(key, None))
)
result_res = draws + border
return result_res
def _generate_trial_id():
s = str(time.time()) + str(random.randint(1, int(1e7)))
return hashlib.sha256(s.encode("utf-8")).hexdigest()[:32]
@@ -0,0 +1,156 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Notice that the following codes are modified from KerasTuner to implement our own tuner.
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/hyperparameters.py.
from .tunable_variable import Boolean, Choice, Fixed, FloatRange, IntRange
class TunableSpace:
"""
A TunableSpace is constructed by the tunable variables.
"""
def __init__(self):
# Tunable variables for this tunable variables
self._variables = {}
# Specific values corresponding to each tunable variable
self._values = {}
@property
def variables(self):
return self._variables
@variables.setter
def variables(self, variables):
self._variables = variables
@property
def values(self):
return self._values
@values.setter
def values(self, values):
self._values = values
def get_value(self, name):
if name in self.values:
return self.values[name]
else:
raise KeyError(f"{name} does not exist.")
def set_value(self, name, value):
if name in self.values:
self.values[name] = value
else:
raise KeyError(f"{name} does not exist.")
def _exists(self, name):
if name in self._variables:
return True
return False
def _retrieve(self, tv):
tv = tv.__class__.from_state(tv.get_state())
if self._exists(tv.name):
return self.get_value(tv.name)
return self._register(tv)
def _register(self, tv):
self._variables[tv.name] = tv
if tv.name not in self.values:
self.values[tv.name] = tv.default
return self.values[tv.name]
def __getitem__(self, name):
return self.get_value(name)
def __setitem__(self, name, value):
self.set_value(name, value)
def __contains__(self, name):
try:
self.get_value(name)
return True
except (KeyError, ValueError):
return False
def fixed(self, name, default):
tv = Fixed(name=name, default=default)
return self._retrieve(tv)
def boolean(self, name, default=False):
tv = Boolean(name=name, default=default)
return self._retrieve(tv)
def choice(self, name, values, default=None):
tv = Choice(name=name, values=values, default=default)
return self._retrieve(tv)
def int_range(self, name, start, stop, step=1, default=None):
tv = IntRange(
name=name, start=start, stop=stop, step=step, default=default
)
return self._retrieve(tv)
def float_range(self, name, start, stop, step=None, default=None):
tv = FloatRange(
name=name, start=start, stop=stop, step=step, default=default
)
return self._retrieve(tv)
def get_state(self):
return {
"variables": [
{"class_name": v.__class__.__name__, "state": v.get_state()}
for v in self._variables.values()
],
"values": dict(self.values.items()),
}
@classmethod
def from_state(cls, state):
ts = cls()
for v in state["variables"]:
v = _deserialize_tunable_variable(v)
ts._variables[v.name] = v
ts._values = dict(state["values"].items())
return ts
def _deserialize_tunable_variable(state):
classes = (Boolean, Fixed, Choice, IntRange, FloatRange)
cls_name_to_cls = {cls.__name__: cls for cls in classes}
if isinstance(state, classes):
return state
if (
not isinstance(state, dict)
or "class_name" not in state
or "state" not in state
):
raise ValueError(
f"Expect state to be a python dict containing class_name and state as keys, but found {state}"
)
cls_name = state["class_name"]
cls = cls_name_to_cls[cls_name]
if cls is None:
raise ValueError(f"Unknown class name {cls_name}")
cls_state = state["state"]
deserialized_object = cls.from_state(cls_state)
return deserialized_object
@@ -0,0 +1,240 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Notice that the following codes are modified from KerasTuner to implement our own tuner.
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/hyperparameters.py.
import numpy as np
class TunableVariable:
"""
TunableVariable base class.
"""
def __init__(self, name, default=None):
self.name = name
self._default = default
@property
def default(self):
return self._default
def get_state(self):
return {"name": self.name, "default": self.default}
@classmethod
def from_state(cls, state):
return cls(**state)
class Fixed(TunableVariable):
"""
Fixed variable which cannot be changed.
"""
def __init__(self, name, default):
super().__init__(name=name, default=default)
self.name = name
if not isinstance(default, (str, int, float, bool)):
raise ValueError(
f"Fixed must be an str, int, float or bool, but found {default}"
)
self._default = default
def random(self, seed=None):
return self._default
def __repr__(self):
return f"Fixed(name: {self.name}, value: {self.default})"
class Boolean(TunableVariable):
"""
Choice between True and False.
"""
def __init__(self, name, default=False):
super().__init__(name=name, default=default)
if default not in {True, False}:
raise ValueError(
f"default must be a Python boolean, but got {default}"
)
def random(self, seed=None):
rng = np.random.default_rng(seed)
return rng.choice((True, False))
def __repr__(self):
return f'Boolean(name: "{self.name}", default: {self.default})'
class Choice(TunableVariable):
def __init__(self, name, values, default=None):
super().__init__(name=name, default=default)
types = {type(v) for v in values}
if len(types) > 1:
raise TypeError(
f"Choice can contain only one type of value, but found values: {values} with types: {types}."
)
self._is_unknown_type = False
if isinstance(values[0], str):
values = [str(v) for v in values]
if default is not None:
default = str(default)
elif isinstance(values[0], int):
values = [int(v) for v in values]
if default is not None:
default = int(default)
elif isinstance(values[0], float):
values = [float(v) for v in values]
if default is not None:
default = float(default)
elif isinstance(values[0], bool):
values = [bool(v) for v in values]
if default is not None:
default = bool(default)
else:
self._is_unknown_type = True
self._indices = list(range(len(values)))
self.values = values
if default is not None and default not in values:
raise ValueError(
f"The default value should be one of the choices {values}, but found {default}"
)
self._default = default
@property
def default(self):
if self._default is None:
if None in self.values:
return None
return self.values[0]
return self._default
def random(self, seed=None):
rng = np.random.default_rng(seed)
if self._is_unknown_type:
indice = rng.choice(self._indices)
return self.values[indice]
else:
return rng.choice(self.values)
def get_state(self):
state = super().get_state()
state["values"] = self.values
return state
def __repr__(self):
return f'Choice(name: "{self.name}", values: {self.values}, default: {self.default})'
class IntRange(TunableVariable):
"""
Integer range.
"""
def __init__(self, name, start, stop, step=1, default=None, endpoint=False):
super().__init__(name=name, default=default)
self.start = self._check_int(start)
self.stop = self._check_int(stop)
self.step = self._check_int(step)
self._default = default
self.endpoint = endpoint
@property
def default(self):
if self._default is not None:
return self._default
return self.start
def random(self, seed=None):
rng = np.random.default_rng(seed)
value = (self.stop - self.start) * rng.random() + self.start
if self.step is not None:
if self.endpoint:
values = np.arange(self.start, self.stop + 1e-7, step=self.step)
else:
values = np.arange(self.start, self.stop, step=self.step)
closest_index = np.abs(values - value).argmin()
value = values[closest_index]
return int(value)
def get_state(self):
state = super().get_state()
state["start"] = self.start
state["stop"] = self.stop
state["step"] = self.step
state["default"] = self._default
return state
def _check_int(self, val):
int_val = int(val)
if int_val != val:
raise ValueError(f"Expects val is an int, but found: {val}.")
return int_val
def __repr__(self):
return f"IntRange(name: {self.name}, start: {self.start}, stop: {self.stop}, step: {self.step}, default: {self.default})"
class FloatRange(TunableVariable):
"""
Float range.
"""
def __init__(
self, name, start, stop, step=None, default=None, endpoint=False
):
super().__init__(name=name, default=default)
self.stop = float(stop)
self.start = float(start)
if step is not None:
self.step = float(step)
else:
self.step = None
self._default = default
self.endpoint = endpoint
@property
def default(self):
if self._default is not None:
return self._default
return self.start
def random(self, seed=None):
rng = np.random.default_rng(seed)
value = (self.stop - self.start) * rng.random() + self.start
if self.step is not None:
if self.endpoint:
values = np.arange(self.start, self.stop + 1e-7, step=self.step)
else:
values = np.arange(self.start, self.stop, step=self.step)
closest_index = np.abs(values - value).argmin()
value = values[closest_index]
return value
def get_state(self):
state = super().get_state()
state["start"] = self.start
state["stop"] = self.stop
state["step"] = self.step
state["endpoint"] = self.endpoint
return state
def __repr__(self):
return f"FloatRange(name: {self.name}, start: {self.start}, stop: {self.stop}, step: {self.step}, default: {self.default}, endpoint: {self.endpoint})"