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,25 @@
# Copyright (c) 2020 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 . import functional # noqa: F401
from .distributed_fused_lamb import DistributedFusedLamb # noqa: F401
from .gradient_merge import GradientMergeOptimizer # noqa: F401
from .lars_momentum import LarsMomentumOptimizer # noqa: F401
from .lbfgs import LBFGS
from .lookahead import LookAhead # noqa: F401
from .modelaverage import ModelAverage # noqa: F401
from .pipeline import PipelineOptimizer # noqa: F401
from .recompute import RecomputeOptimizer # noqa: F401
__all__ = ['LBFGS']
@@ -0,0 +1,516 @@
# Copyright (c) 2021 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 os
import paddle
from paddle.base import core, unique_name
from paddle.base.executor import global_scope
from paddle.base.framework import Variable, name_scope
from paddle.base.layer_helper import LayerHelper
from paddle.nn import ClipGradByGlobalNorm
from paddle.optimizer import Optimizer
def init_communicator(block, rank, ranks, ring_id):
eps = os.environ['PADDLE_TRAINER_ENDPOINTS']
eps = [ep.strip() for ep in eps.split(",") if ep.strip()]
cur_ep = eps[rank]
other_eps = [eps[r] for r in ranks if r != rank]
local_rank = ranks.index(rank)
comm_var_name = unique_name.generate('comm_id')
comm_id_var = block.create_var(
name=comm_var_name, persistable=True, type=core.VarDesc.VarType.RAW
)
if core.is_compiled_with_cuda():
block.append_op(
type='c_gen_nccl_id',
inputs={},
outputs={'Out': comm_id_var},
attrs={
'rank': local_rank,
'endpoint': cur_ep,
'other_endpoints': other_eps,
'ring_id': ring_id,
},
)
elif core.is_compiled_with_xpu():
block.append_op(
type='c_gen_bkcl_id',
inputs={},
outputs={'Out': comm_id_var},
attrs={
'rank': local_rank,
'endpoint': cur_ep,
'other_endpoints': other_eps,
'ring_id': ring_id,
},
)
elif (
paddle.distributed.ParallelEnv().device_type
in paddle.device.get_all_custom_device_type()
):
block.append_op(
type='c_gen_xccl_id',
inputs={},
outputs={'Out': comm_id_var},
attrs={
'rank': local_rank,
'endpoint': cur_ep,
'other_endpoints': other_eps,
'ring_id': ring_id,
},
)
block.append_op(
type='c_comm_init',
inputs={'X': comm_id_var},
outputs={},
attrs={
'nranks': len(ranks),
'rank': local_rank,
'ring_id': ring_id,
'endpoints': ','.join(eps),
},
)
tmp_var = block.create_var(name=unique_name.generate('tmp'))
block.append_op(
type='fill_constant', outputs={'Out': tmp_var}, attrs={'value': 1}
)
block.append_op(
type='all_reduce',
inputs={'x': tmp_var},
outputs={'out': tmp_var},
attrs={
'ring_id': ring_id,
'reduce_type': paddle.distributed.ReduceOp.SUM,
},
)
block.append_op(
type='c_sync_calc_stream',
inputs={'X': tmp_var},
outputs={'Out': tmp_var},
)
return ring_id
def broadcast_parameters(block, parameters, ring_id):
for p in parameters:
block.append_op(
type='broadcast',
inputs={'x': p},
outputs={'out': p},
attrs={
'ring_id': ring_id,
},
)
class DistributedFusedLamb(Optimizer):
def __init__(
self,
learning_rate=0.001,
lamb_weight_decay=0.01,
beta1=0.9,
beta2=0.999,
epsilon=1e-6,
parameters=None,
grad_clip=None,
exclude_from_weight_decay_fn=None,
clip_after_allreduce=True,
is_grad_scaled_by_nranks=True,
alignment=128,
use_master_param_norm=True,
gradient_accumulation_steps=1,
use_master_acc_grad=True,
nproc_per_node=None,
use_hierarchical_allreduce=False,
name=None,
):
assert not paddle.in_dynamic_mode(), (
"DistributedFusedLamb does not support dygraph mode"
)
super().__init__(learning_rate=learning_rate, grad_clip=None, name=name)
self._beta1 = beta1
self._beta2 = beta2
self._epsilon = epsilon
self._weight_decay = (
lamb_weight_decay if lamb_weight_decay is not None else 0.0
)
if grad_clip is not None:
assert isinstance(grad_clip, ClipGradByGlobalNorm), (
"Only ClipGradByGlobalNorm is supported in DistributedFusedLamb"
)
max_global_grad_norm = grad_clip.clip_norm
else:
max_global_grad_norm = -1.0
self._max_global_grad_norm = max_global_grad_norm
self._alignment = alignment if alignment is not None else -1
self._clip_after_allreduce = clip_after_allreduce
self._is_grad_scaled_by_nranks = is_grad_scaled_by_nranks
self._exclude_from_weight_decay_fn = exclude_from_weight_decay_fn
self._scale = None
self._use_master_param_norm = use_master_param_norm
self._gradient_accumulation_steps = gradient_accumulation_steps
self._use_master_acc_grad = use_master_acc_grad
self._nproc_per_node = nproc_per_node
self._use_hierarchical_allreduce = use_hierarchical_allreduce
assert self._gradient_accumulation_steps >= 1
self.helper = LayerHelper('distributed_fused_lamb')
self._supports_check_nan_inf = True # very import flag for AMP
main_block = self.helper.main_program.global_block()
self._found_inf = main_block.create_var(
name=unique_name.generate('found_inf'),
shape=[1],
dtype=core.VarDesc.VarType.BOOL,
)
self._step = None
if self._gradient_accumulation_steps > 1:
self._stop_update = main_block.create_var(
name=unique_name.generate('stop_update'),
shape=[1],
dtype=core.VarDesc.VarType.BOOL,
)
else:
self._stop_update = None
self._param_to_master_param = {}
def _get_stop_update_var(self):
return self._stop_update if self._stop_update is not None else False
def _set_step(self, step):
self._step = step
def _get_or_create_step(self):
if self._step is None:
self._step = self._create_persistable_var('step', dtype='int64')
return self._step
def _set_scale(self, scale):
assert scale is not None
if not isinstance(scale, Variable):
scale = self._create_scale_from_constant(scale)
self._scale = scale
def _create_scale_from_constant(self, value):
name = unique_name.generate('global_scale')
return paddle.static.create_global_var(
name=name,
shape=[1],
dtype='float32',
value=float(value),
persistable=True,
)
def _get_or_create_scale(self):
if self._scale is None:
self._scale = self._create_scale_from_constant(1.0)
return self._scale
def _create_persistable_var(self, name=None, shape=[-1], dtype='float32'):
startup_block = self.helper.startup_program.global_block()
if name is not None:
name = unique_name.generate(name)
startup_var = startup_block.create_var(
name=name,
shape=shape,
dtype=dtype,
persistable=True,
stop_gradient=True,
)
main_block = self.helper.main_program.global_block()
main_var = main_block.create_var(
name=startup_var.name,
shape=startup_var.shape,
dtype=startup_var.dtype,
persistable=True,
stop_gradient=True,
)
return main_var
def _get_parameter(self, name, scope=None):
if scope is None:
scope = global_scope()
master_param = self._param_to_master_param.get(name)
assert master_param is not None
master_param_t = scope.find_var(master_param).get_tensor()
assert master_param_t._dtype() == paddle.float32
param_t = scope.find_var(name).get_tensor()
if param_t._dtype() == paddle.float32:
assert param_t._ptr() == master_param_t._ptr()
return param_t, None
else:
assert param_t._dtype() == paddle.float16
assert param_t.shape() == master_param_t.shape()
return param_t, master_param_t
def apply_optimize(self, params_grads):
self.apply_gradients(params_grads)
def apply_gradients(self, params_grads):
flattened = []
for p, g in params_grads:
flattened.extend([p, g])
with (
flattened[0].block.program._optimized_guard(flattened),
name_scope("optimizer"),
):
self._apply_gradients_impl(params_grads)
def _apply_gradients_impl(self, params_grads):
for p, g in params_grads:
assert g.type == core.VarDesc.VarType.DENSE_TENSOR, (
"Only support dense gradient"
)
g.persistable = True # the gradient must be persistable for fusion
fp32_fused_param = self._create_persistable_var('fp32_fused_param')
fp32_fused_grad = self._create_persistable_var('fp32_fused_grad')
fp16_fused_param = self._create_persistable_var(
'fp16_fused_param', dtype='float16'
)
fp16_fused_grad = self._create_persistable_var(
'fp16_fused_grad', dtype='float16'
)
master_params = []
for p, g in params_grads:
master_p = self._create_persistable_var('master_weight')
self._param_to_master_param[p.name] = master_p.name
master_params.append(master_p)
moment1 = self._create_persistable_var('moment1')
moment1.is_distributed = True
moment2 = self._create_persistable_var('moment2')
moment2.is_distributed = True
beta1pow = self._create_persistable_var('beta1pow')
beta2pow = self._create_persistable_var('beta2pow')
param_info = self._create_persistable_var('param_info', dtype='int32')
param_info.is_distributed = True
fused_offsets = self._create_persistable_var(
'fused_offsets', dtype='int32'
)
fp32_partial_fused_offsets = self._create_persistable_var(
'fp32_partial_fused_offsets', dtype='int32'
)
fp32_partial_fused_offsets.is_distributed = True
fp16_partial_fused_offsets = self._create_persistable_var(
'fp16_partial_fused_offsets', dtype='int32'
)
fp16_partial_fused_offsets.is_distributed = True
param_order = self._create_persistable_var('param_order', dtype='int32')
param_order.is_distributed = True
if self._gradient_accumulation_steps > 1:
fp32_acc_fused_grad = [
self._create_persistable_var('fp32_acc_fused_grad')
]
fp16_acc_fused_grad = [
self._create_persistable_var(
'fp16_acc_fused_grad', dtype='float16'
)
]
acc_step = [self._create_persistable_var('acc_step', dtype='int64')]
else:
fp32_acc_fused_grad = []
fp16_acc_fused_grad = []
acc_step = []
step = self._get_or_create_step()
rank = paddle.distributed.get_rank()
nranks = paddle.distributed.get_world_size()
if self._nproc_per_node is None:
nproc_per_node = nranks
else:
nproc_per_node = self._nproc_per_node
assert nranks % nproc_per_node == 0, (
"nranks should be exactly divided by nproc_per_node"
)
shard_inside_node = nranks > nproc_per_node
local_rank = rank % nproc_per_node
node_id = int(rank / nproc_per_node)
node_num = int(nranks / nproc_per_node)
ring_ids = []
startup_block = self.helper.startup_program.global_block()
if nranks > 1:
ring_id = init_communicator(
startup_block, rank, list(range(nranks)), 0
)
ring_ids.append(ring_id)
use_hierarchical_allreduce = False
if node_num > 1 and len(ring_ids) <= 1 and shard_inside_node:
local_group_ranks = list(
range(node_id * nproc_per_node, (node_id + 1) * nproc_per_node)
)
ring_id = init_communicator(
startup_block, rank, local_group_ranks, 1
)
ring_ids.append(ring_id)
if self._use_hierarchical_allreduce and nranks > nproc_per_node:
use_hierarchical_allreduce = True
outer_group_ranks = list(
range(rank % nproc_per_node, nranks, nproc_per_node)
)
ring_id = init_communicator(
startup_block, rank, outer_group_ranks, ring_ids[-1] + 1
)
ring_ids.append(ring_id)
scale = self._get_or_create_scale()
params = [p for p, _ in params_grads]
grads = [g for _, g in params_grads]
apply_weight_decay = [1] * len(params)
if self._exclude_from_weight_decay_fn is not None:
for i, p in enumerate(params):
if self._exclude_from_weight_decay_fn(p):
apply_weight_decay[i] = 0
for g in grads:
startup_block.create_var(
name=g.name,
type=g.type,
dtype=g.dtype,
persistable=g.persistable,
shape=g.shape,
)
if nranks > 1:
broadcast_parameters(startup_block, params, ring_ids[0])
startup_block.append_op(
type='distributed_fused_lamb_init',
inputs={
'Param': params,
'Grad': grads,
},
outputs={
'FP32FusedParam': [fp32_fused_param],
'FP32FusedGrad': [fp32_fused_grad],
'FP16FusedParam': [fp16_fused_param],
'FP16FusedGrad': [fp16_fused_grad],
'Moment1': [moment1],
'Moment2': [moment2],
'Beta1Pow': [beta1pow],
'Beta2Pow': [beta2pow],
'GlobalScale': [scale],
'ParamInfo': [param_info],
'ParamOut': params,
'MasterParamOut': master_params,
'GradOut': grads,
'FP32ShardFusedParamOffsets': [fp32_partial_fused_offsets],
'FP16ShardFusedParamOffsets': [fp16_partial_fused_offsets],
'FusedParamOffsets': [fused_offsets],
'ParamOrder': [param_order],
'Step': [step],
},
attrs={
'alignment': self._alignment,
'rank': local_rank if shard_inside_node else rank,
'nranks': nproc_per_node if shard_inside_node else nranks,
'apply_weight_decay': apply_weight_decay,
'moment1': 0.0,
'moment2': 0.0,
'beta1': self._beta1,
'beta2': self._beta2,
},
)
main_block = self.helper.main_program.global_block()
self._create_global_learning_rate()
lr = None
for p_g in params_grads:
if lr is None:
lr = self._create_param_lr(p_g)
else:
new_lr = self._create_param_lr(p_g)
assert id(lr) == id(new_lr), (
"The learning rate for each parameter should be the same"
)
assert lr is not None
lamb_op = main_block.append_op(
type='distributed_fused_lamb',
inputs={
'FP32FusedParam': [fp32_fused_param],
'FP32FusedGrad': [fp32_fused_grad],
'FP16FusedParam': [fp16_fused_param],
'FP16FusedGrad': [fp16_fused_grad],
'LearningRate': [lr],
'Moment1': [moment1],
'Moment2': [moment2],
'Beta1Pow': [beta1pow],
'Beta2Pow': [beta2pow],
'GlobalScale': [scale],
'ParamInfo': [param_info],
'Param': params,
'Grad': grads,
'FusedParamOffsets': [fused_offsets],
'FP32ShardFusedParamOffsets': [fp32_partial_fused_offsets],
'FP16ShardFusedParamOffsets': [fp16_partial_fused_offsets],
'ParamOrder': [param_order],
},
outputs={
'FP32FusedParamOut': [fp32_fused_param],
'FP16FusedParamOut': [fp16_fused_param],
'Moment1Out': [moment1],
'Moment2Out': [moment2],
'Beta1PowOut': [beta1pow],
'Beta2PowOut': [beta2pow],
'ParamOut': params,
'GradOut': grads,
'FoundInf': [self._found_inf],
'FP32AccFusedGrad': fp32_acc_fused_grad,
'FP16AccFusedGrad': fp16_acc_fused_grad,
'AccStep': acc_step,
'StopUpdate': (
self._stop_update if self._stop_update is not None else []
),
'Step': [step],
},
attrs={
'weight_decay': self._weight_decay,
'beta1': self._beta1,
'beta2': self._beta2,
'epsilon': self._epsilon,
'max_global_grad_norm': self._max_global_grad_norm,
'clip_after_allreduce': self._clip_after_allreduce,
'rank': rank,
'nranks': nranks,
'ring_ids': ring_ids,
'use_master_param_norm': self._use_master_param_norm,
'is_grad_scaled_by_nranks': self._is_grad_scaled_by_nranks,
'acc_steps': self._gradient_accumulation_steps,
'use_master_acc_grad': self._use_master_acc_grad,
'use_hierarchical_allreduce': use_hierarchical_allreduce,
},
)
return [lamb_op]
@@ -0,0 +1,18 @@
# Copyright (c) 2021 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 .bfgs import minimize_bfgs
from .lbfgs import minimize_lbfgs
__all__ = ['minimize_bfgs', 'minimize_lbfgs']
@@ -0,0 +1,231 @@
# 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 __future__ import annotations
from typing import TYPE_CHECKING, Literal
import numpy as np
import paddle
from .line_search import strong_wolfe
from .utils import (
_value_and_gradient,
check_initial_inverse_hessian_estimate,
check_input_type,
)
if TYPE_CHECKING:
from collections.abc import Callable
from paddle import Tensor
def minimize_bfgs(
objective_func: Callable[[Tensor], Tensor],
initial_position: Tensor,
max_iters: int = 50,
tolerance_grad: float = 1e-7,
tolerance_change: float = 1e-9,
initial_inverse_hessian_estimate: Tensor | None = None,
line_search_fn: Literal['strong_wolfe'] = 'strong_wolfe',
max_line_search_iters: int = 50,
initial_step_length: float = 1.0,
dtype: Literal['float32', 'float64'] = 'float32',
name: str | None = None,
) -> tuple[bool, int, Tensor, Tensor, Tensor, Tensor]:
r"""
Minimizes a differentiable function `func` using the BFGS method.
The BFGS is a quasi-Newton method for solving an unconstrained optimization problem over a differentiable function.
Closely related is the Newton method for minimization. Consider the iterate update formula:
.. math::
x_{k+1} = x_{k} + H_k \nabla{f_k}
If :math:`H_k` is the inverse Hessian of :math:`f` at :math:`x_k`, then it's the Newton method.
If :math:`H_k` is symmetric and positive definite, used as an approximation of the inverse Hessian, then
it's a quasi-Newton. In practice, the approximated Hessians are obtained
by only using the gradients, over either whole or part of the search
history, the former is BFGS, the latter is L-BFGS.
Reference:
Jorge Nocedal, Stephen J. Wright, Numerical Optimization, Second Edition, 2006. pp140: Algorithm 6.1 (BFGS Method).
Args:
objective_func: the objective function to minimize. ``objective_func`` accepts a 1D Tensor and returns a scalar.
initial_position (Tensor): the starting point of the iterates, has the same shape with the input of ``objective_func`` .
max_iters (int, optional): the maximum number of minimization iterations. Default value: 50.
tolerance_grad (float, optional): terminates if the gradient norm is smaller than this. Currently gradient norm uses inf norm. Default value: 1e-7.
tolerance_change (float, optional): terminates if the change of function value/position/parameter between two iterations is smaller than this value. Default value: 1e-9.
initial_inverse_hessian_estimate (Tensor, optional): the initial inverse hessian approximation at initial_position. It must be symmetric and positive definite. If not given, will use an identity matrix of order N, which is size of ``initial_position`` . Default value: None.
line_search_fn (str, optional): indicate which line search method to use, only support 'strong wolfe' right now. May support 'Hager Zhang' in the future. Default value: 'strong wolfe'.
max_line_search_iters (int, optional): the maximum number of line search iterations. Default value: 50.
initial_step_length (float, optional): step length used in first iteration of line search. different initial_step_length may cause different optimal result. For methods like Newton and quasi-Newton the initial trial step length should always be 1.0. Default value: 1.0.
dtype ('float32' | 'float64', optional): data type used in the algorithm, the data type of the input parameter must be consistent with the dtype. Default value: 'float32'.
name (str, optional): Name for the operation. For more information, please refer to :ref:`api_guide_Name`. Default value: None.
Returns:
output(tuple):
- is_converge (bool): Indicates whether found the minimum within tolerance.
- num_func_calls (int): number of objective function called.
- position (Tensor): the position of the last iteration. If the search converged, this value is the argmin of the objective function regarding to the initial position.
- objective_value (Tensor): objective function value at the `position`.
- objective_gradient (Tensor): objective function gradient at the `position`.
- inverse_hessian_estimate (Tensor): the estimate of inverse hessian at the `position`.
Examples:
.. code-block:: pycon
:name: code-example1
>>> # Example1: 1D Grid Parameters
>>> import paddle
>>> # Randomly simulate a batch of input data
>>> inputs = paddle.normal(shape=(100, 1))
>>> labels = inputs * 2.0
>>> # define the loss function
>>> def loss(w):
... y = w * inputs
... return paddle.nn.functional.square_error_cost(y, labels).mean()
>>> # Initialize weight parameters
>>> w = paddle.normal(shape=(1,))
>>> # Call the bfgs method to solve the weight that makes the loss the smallest, and update the parameters
>>> for epoch in range(0, 10):
... # Call the bfgs method to optimize the loss, note that the third parameter returned represents the weight
... w_update = paddle.incubate.optimizer.functional.minimize_bfgs(loss, w)[2]
... # Use paddle.assign to update parameters in place
... paddle.assign(w_update, w)
.. code-block:: pycon
:name: code-example2
>>> # Example2: Multidimensional Grid Parameters
>>> import paddle
>>> def flatten(x):
... return x.flatten()
>>> def unflatten(x):
... return x.reshape((2, 2))
>>> # Assume the network parameters are more than one dimension
>>> def net(x):
... assert len(x.shape) > 1
... return x.square().mean()
>>> # function to be optimized
>>> def bfgs_f(flatten_x):
... return net(unflatten(flatten_x))
>>> x = paddle.rand([2, 2])
>>> for i in range(0, 10):
... # Flatten x before using minimize_bfgs
... x_update = paddle.incubate.optimizer.functional.minimize_bfgs(bfgs_f, flatten(x))[2]
... # unflatten x_update, then update parameters
... paddle.assign(unflatten(x_update), x)
"""
if dtype not in ['float32', 'float64']:
raise ValueError(
f"The dtype must be 'float32' or 'float64', but the specified is {dtype}."
)
op_name = 'minimize_bfgs'
check_input_type(initial_position, 'initial_position', op_name)
I = paddle.eye(initial_position.shape[0], dtype=dtype)
if initial_inverse_hessian_estimate is None:
initial_inverse_hessian_estimate = I
else:
check_input_type(
initial_inverse_hessian_estimate,
'initial_inverse_hessian_estimate',
op_name,
)
check_initial_inverse_hessian_estimate(initial_inverse_hessian_estimate)
Hk = paddle.assign(initial_inverse_hessian_estimate)
# use detach and assign to create new tensor rather than =, or xk will share memory and grad with initial_position
xk = paddle.assign(initial_position.detach())
value, g1 = _value_and_gradient(objective_func, xk)
num_func_calls = paddle.full(shape=[1], fill_value=1, dtype='int64')
# when the dim of x is 1000, it needs more than 30 iters to get all element converge to minimum.
k = paddle.full(shape=[1], fill_value=0, dtype='int64')
done = paddle.full(shape=[1], fill_value=False, dtype='bool')
is_converge = paddle.full(shape=[1], fill_value=False, dtype='bool')
def cond(k, done, is_converge, num_func_calls, xk, value, g1, Hk):
return (k < max_iters) & ~done
def body(k, done, is_converge, num_func_calls, xk, value, g1, Hk):
# -------------- compute pk -------------- #
pk = -paddle.matmul(Hk, g1)
# -------------- compute alpha by line search -------------- #
if line_search_fn == 'strong_wolfe':
alpha, value, g2, ls_func_calls = strong_wolfe(
f=objective_func,
xk=xk,
pk=pk,
max_iters=max_line_search_iters,
initial_step_length=initial_step_length,
dtype=dtype,
)
else:
raise NotImplementedError(
f"Currently only support line_search_fn = 'strong_wolfe', but the specified is '{line_search_fn}'"
)
num_func_calls += ls_func_calls
# -------------- update Hk -------------- #
sk = alpha * pk
yk = g2 - g1
xk = xk + sk
g1 = g2
sk = paddle.unsqueeze(sk, 0)
yk = paddle.unsqueeze(yk, 0)
rhok_inv = paddle.dot(yk, sk)
rhok = paddle.static.nn.cond(
rhok_inv == 0.0,
lambda: paddle.full(shape=[1], fill_value=1000.0, dtype=dtype),
lambda: 1.0 / rhok_inv,
)
Vk_transpose = I - rhok * sk * yk.t()
Vk = I - rhok * yk * sk.t()
Hk = (
paddle.matmul(paddle.matmul(Vk_transpose, Hk), Vk)
+ rhok * sk * sk.t()
)
k += 1
# -------------- check convergence -------------- #
gnorm = paddle.linalg.norm(g1, p=np.inf)
pk_norm = paddle.linalg.norm(pk, p=np.inf)
paddle.assign(
done | (gnorm < tolerance_grad) | (pk_norm < tolerance_change), done
)
paddle.assign(done, is_converge)
# when alpha=0, there is no chance to get xk change.
paddle.assign(done | (alpha == 0.0), done)
return [k, done, is_converge, num_func_calls, xk, value, g1, Hk]
paddle.static.nn.while_loop(
cond=cond,
body=body,
loop_vars=[k, done, is_converge, num_func_calls, xk, value, g1, Hk],
)
return is_converge, num_func_calls, xk, value, g1, Hk
@@ -0,0 +1,341 @@
# 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 __future__ import annotations
from typing import TYPE_CHECKING, Literal
import numpy as np
import paddle
from .line_search import strong_wolfe
from .utils import (
_value_and_gradient,
check_initial_inverse_hessian_estimate,
check_input_type,
)
if TYPE_CHECKING:
from collections.abc import Callable
from paddle import Tensor
def minimize_lbfgs(
objective_func: Callable[[Tensor], Tensor],
initial_position: Tensor,
history_size: int = 100,
max_iters: int = 50,
tolerance_grad: float = 1e-8,
tolerance_change: float = 1e-8,
initial_inverse_hessian_estimate: Tensor | None = None,
line_search_fn: Literal['strong_wolfe'] = 'strong_wolfe',
max_line_search_iters: int = 50,
initial_step_length: int = 1.0,
dtype: Literal['float32', 'float64'] = 'float32',
name: str | None = None,
) -> tuple[bool, int, Tensor, Tensor, Tensor]:
r"""
Minimizes a differentiable function `func` using the L-BFGS method.
The L-BFGS is a quasi-Newton method for solving an unconstrained optimization problem over a differentiable function.
Closely related is the Newton method for minimization. Consider the iterate update formula:
.. math::
x_{k+1} = x_{k} + H_k \nabla{f_k}
If :math:`H_k` is the inverse Hessian of :math:`f` at :math:`x_k`, then it's the Newton method.
If :math:`H_k` is symmetric and positive definite, used as an approximation of the inverse Hessian, then
it's a quasi-Newton. In practice, the approximated Hessians are obtained
by only using the gradients, over either whole or part of the search
history, the former is BFGS, the latter is L-BFGS.
Reference:
Jorge Nocedal, Stephen J. Wright, Numerical Optimization, Second Edition, 2006. pp179: Algorithm 7.5 (L-BFGS).
Args:
objective_func: the objective function to minimize. ``objective_func`` accepts a 1D Tensor and returns a scalar.
initial_position (Tensor): the starting point of the iterates, has the same shape with the input of ``objective_func`` .
history_size (Scalar): the number of stored vector pairs {si,yi}. Default value: 100.
max_iters (int, optional): the maximum number of minimization iterations. Default value: 50.
tolerance_grad (float, optional): terminates if the gradient norm is smaller than this. Currently gradient norm uses inf norm. Default value: 1e-7.
tolerance_change (float, optional): terminates if the change of function value/position/parameter between two iterations is smaller than this value. Default value: 1e-9.
initial_inverse_hessian_estimate (Tensor, optional): the initial inverse hessian approximation at initial_position. It must be symmetric and positive definite. If not given, will use an identity matrix of order N, which is size of ``initial_position`` . Default value: None.
line_search_fn (str, optional): indicate which line search method to use, only support 'strong wolfe' right now. May support 'Hager Zhang' in the future. Default value: 'strong wolfe'.
max_line_search_iters (int, optional): the maximum number of line search iterations. Default value: 50.
initial_step_length (float, optional): step length used in first iteration of line search. different initial_step_length may cause different optimal result. For methods like Newton and quasi-Newton the initial trial step length should always be 1.0. Default value: 1.0.
dtype ('float32' | 'float64', optional): data type used in the algorithm, the data type of the input parameter must be consistent with the dtype. Default value: 'float32'.
name (str, optional): Name for the operation. For more information, please refer to :ref:`api_guide_Name`. Default value: None.
Returns:
output(tuple):
- is_converge (bool): Indicates whether found the minimum within tolerance.
- num_func_calls (int): number of objective function called.
- position (Tensor): the position of the last iteration. If the search converged, this value is the argmin of the objective function regarding to the initial position.
- objective_value (Tensor): objective function value at the `position`.
- objective_gradient (Tensor): objective function gradient at the `position`.
Examples:
.. code-block:: pycon
:name: code-example1
>>> # Example1: 1D Grid Parameters
>>> import paddle
>>> # Randomly simulate a batch of input data
>>> inputs = paddle.normal(shape=(100, 1))
>>> labels = inputs * 2.0
>>> # define the loss function
>>> def loss(w):
... y = w * inputs
... return paddle.nn.functional.square_error_cost(y, labels).mean()
>>> # Initialize weight parameters
>>> w = paddle.normal(shape=(1,))
>>> # Call the bfgs method to solve the weight that makes the loss the smallest, and update the parameters
>>> for epoch in range(0, 10):
... # Call the bfgs method to optimize the loss, note that the third parameter returned represents the weight
... w_update = paddle.incubate.optimizer.functional.minimize_bfgs(loss, w)[2]
... # Use paddle.assign to update parameters in place
... paddle.assign(w_update, w)
.. code-block:: pycon
:name: code-example2
>>> # Example2: Multidimensional Grid Parameters
>>> import paddle
>>> def flatten(x):
... return x.flatten()
>>> def unflatten(x):
... return x.reshape((2, 2))
>>> # Assume the network parameters are more than one dimension
>>> def net(x):
... assert len(x.shape) > 1
... return x.square().mean()
>>> # function to be optimized
>>> def bfgs_f(flatten_x):
... return net(unflatten(flatten_x))
>>> x = paddle.rand([2, 2])
>>> for i in range(0, 10):
... # Flatten x before using minimize_bfgs
... x_update = paddle.incubate.optimizer.functional.minimize_bfgs(bfgs_f, flatten(x))[2]
... # unflatten x_update, then update parameters
... paddle.assign(unflatten(x_update), x)
"""
if dtype not in ['float32', 'float64']:
raise ValueError(
f"The dtype must be 'float32' or 'float64', but the specified is {dtype}."
)
op_name = 'minimize_lbfgs'
check_input_type(initial_position, 'initial_position', op_name)
if initial_inverse_hessian_estimate is None:
H0 = paddle.eye(initial_position.shape[0], dtype=dtype)
else:
check_input_type(
initial_inverse_hessian_estimate,
'initial_inverse_hessian_estimate',
op_name,
)
check_initial_inverse_hessian_estimate(initial_inverse_hessian_estimate)
H0 = initial_inverse_hessian_estimate
# use detach and assign to create new tensor rather than =, or xk will share memory and grad with initial_position
xk = paddle.assign(initial_position.detach())
value, g1 = _value_and_gradient(objective_func, xk)
k = paddle.full(shape=[1], fill_value=0, dtype='int64')
done = paddle.full(shape=[1], fill_value=False, dtype='bool')
is_converge = paddle.full(shape=[1], fill_value=False, dtype='bool')
num_func_calls = paddle.full(shape=[1], fill_value=1, dtype='int64')
history_size = paddle.full(shape=[], fill_value=history_size, dtype='int64')
head = paddle.full(shape=[1], fill_value=1, dtype='int64')
tail = paddle.full(shape=[1], fill_value=0, dtype='int64')
shape = initial_position.shape[0]
# Use tensor as array of fixed length, rather than flexible tensor array. Because in static graph mode,
# tensor array will produce tensor of shape[-1], which will cause error when calling jacobian. In this way, can not use append
# or pop, so we need head and tail to record where is the newest data and where is the oldest.
# Totally speaking, realized a stack by array.
sk_vec = paddle.zeros((history_size + 1, shape), dtype=dtype)
yk_vec = paddle.zeros((history_size + 1, shape), dtype=dtype)
rhok_vec = paddle.zeros((history_size + 1, 1), dtype=dtype)
ai_vec = paddle.zeros((history_size + 1, 1), dtype=dtype)
def cond(
k,
done,
is_converge,
num_func_calls,
value,
xk,
g1,
sk_vec,
yk_vec,
rhok_vec,
head,
tail,
):
return (k < max_iters) & ~done
def body(
k,
done,
is_converge,
num_func_calls,
value,
xk,
g1,
sk_vec,
yk_vec,
rhok_vec,
head,
tail,
):
# use assign to cut off the relevance between g1 and q, or they will change together.
# -------------- compute p_k by two-loop recursion -------------- #
q = paddle.assign(g1)
# In a array circle, the index may out of range, so must use mod.
i = paddle.full(
shape=[], fill_value=(head - 1).mod(history_size), dtype='int64'
)
def cond(i, q, ai_vec):
return i != tail
def body(i, q, ai_vec):
if paddle.in_dynamic_mode():
ai_vec[i] = rhok_vec[i] * paddle.dot(sk_vec[i], q)
else:
ai_vec = paddle.static.setitem(
ai_vec, i, rhok_vec[i] * paddle.dot(sk_vec[i], q)
)
q = q - ai_vec[i] * yk_vec[i]
i = (i - 1).mod(history_size)
return i, q, ai_vec
paddle.static.nn.while_loop(
cond=cond, body=body, loop_vars=[i, q, ai_vec]
)
r = paddle.matmul(H0, q)
i = paddle.full(shape=[], fill_value=tail + 1, dtype='int64')
def cond(i, r):
return i != head
def body(i, r):
beta = rhok_vec[i] * paddle.dot(yk_vec[i], r)
r = r + sk_vec[i] * (ai_vec[i] - beta)
i = (i + 1).mod(history_size)
return i, r
paddle.static.nn.while_loop(cond=cond, body=body, loop_vars=[i, r])
pk = -r
# -------------- compute alpha by line search -------------- #
if line_search_fn == 'strong_wolfe':
alpha, value, g2, ls_func_calls = strong_wolfe(
f=objective_func,
xk=xk,
pk=pk,
max_iters=max_line_search_iters,
initial_step_length=initial_step_length,
dtype=dtype,
)
else:
raise NotImplementedError(
f"Currently only support line_search_fn = 'strong_wolfe', but the specified is '{line_search_fn}'"
)
paddle.assign(num_func_calls + ls_func_calls, num_func_calls)
# -------------- update sk_vec, yk_vec, rhok_vec -------------- #
sk = alpha * pk
yk = g2 - g1
rhok_inv = paddle.dot(yk, sk)
rhok = paddle.static.nn.cond(
rhok_inv == 0.0,
lambda: paddle.full(shape=[1], fill_value=1000.0, dtype=dtype),
lambda: 1.0 / rhok_inv,
)
if paddle.in_dynamic_mode():
sk_vec[head] = sk
yk_vec[head] = yk
rhok_vec[head] = rhok
else:
sk_vec = paddle.static.setitem(sk_vec, head, sk)
yk_vec = paddle.static.setitem(yk_vec, head, yk)
rhok_vec = paddle.static.setitem(rhok_vec, head, rhok)
head = (head + 1) % history_size
def true_fn(tail):
paddle.assign(tail + 1, tail)
# when array is full, the tail should move forward too.
paddle.static.nn.cond(head == tail, lambda: true_fn(tail), None)
xk = xk + sk
g1 = g2
k += 1
# -------------- check convergence -------------- #
gnorm = paddle.linalg.norm(g1, p=np.inf)
pk_norm = paddle.linalg.norm(pk, p=np.inf)
paddle.assign(
done | (gnorm < tolerance_grad) | (pk_norm < tolerance_change), done
)
paddle.assign(done, is_converge)
# when alpha=0, there is no chance to get xk change.
paddle.assign(done | (alpha == 0.0), done)
return [
k,
done,
is_converge,
num_func_calls,
value,
xk,
g1,
sk_vec,
yk_vec,
rhok_vec,
head,
tail,
]
paddle.static.nn.while_loop(
cond=cond,
body=body,
loop_vars=[
k,
done,
is_converge,
num_func_calls,
value,
xk,
g1,
sk_vec,
yk_vec,
rhok_vec,
head,
tail,
],
)
return is_converge, num_func_calls, xk, value, g1
@@ -0,0 +1,368 @@
# 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 paddle
from .utils import _value_and_gradient
def cubic_interpolation_(x1, f1, g1, x2, f2, g2):
r"""Cubic interpolation between (x1, f1, g1) and (x2, f2, g2).
Use two points and their gradient to determine a cubic function and get the minimum point
between them in the cubic curve.
Reference:
Jorge Nocedal, Stephen J. Wright, Numerical Optimization, Second Edition, 2006.
pp59: formula 3.59
Args:
x1, f1, g1: point1's position, value and gradient.
x2, f2, g2: point2's position, value and gradient.
Returns:
min_pos: the minimum point between the specified points in the cubic curve.
"""
xmin, xmax = paddle.static.nn.cond(
x1 <= x2, lambda: (x1, x2), lambda: (x2, x1)
)
d1 = g1 + g2 - 3 * (f1 - f2) / (x1 - x2)
d2_square = d1**2 - g1 * g2
def true_func1():
d2 = d2_square.sqrt()
def true_fn2():
return x2 - (x2 - x1) * ((g2 + d2 - d1) / (g2 - g1 + 2 * d2))
def false_fn2():
return x1 - (x1 - x2) * ((g1 + d2 - d1) / (g1 - g2 + 2 * d2))
pred = paddle.less_equal(x=x1, y=x2)
min_pos = paddle.static.nn.cond(pred, true_fn2, false_fn2)
return paddle.minimum(paddle.maximum(min_pos, xmin), xmax)
def false_func1():
return (xmin + xmax) / 2.0
min_pos = paddle.static.nn.cond(d2_square >= 0.0, true_func1, false_func1)
return min_pos
def strong_wolfe(
f,
xk,
pk,
max_iters=20,
tolerance_change=1e-8,
initial_step_length=1.0,
c1=1e-4,
c2=0.9,
alpha_max=10,
dtype='float32',
):
r"""Implements of line search algorithm that satisfies the strong Wolfe conditions using double zoom.
Reference:
Jorge Nocedal, Stephen J. Wright, Numerical Optimization, Second Edition, 2006.
pp60: Algorithm 3.5 (Line Search Algorithm).
Args:
f: the objective function to minimize. ``f`` accepts a multivariate input and returns a scalar.
xk (Tensor): the starting point of the iterates.
pk (Tensor): search direction.
max_iters (Scalar): the maximum number of iterations.
tolerance_grad (Scalar): terminates if the gradient norm is smaller than
this. Currently gradient norm uses inf norm.
tolerance_change (Scalar): terminates if the change of function value/position/parameter between
two iterations is smaller than this value.
initial_step_length (Scalar): step length used in first iteration.
c1 (Scalar): parameter for sufficient decrease condition.
c2 (Scalar): parameter for curvature condition.
alpha_max (float): max step length.
dtype ('float32' | 'float64'): the datatype to be used.
Returns:
num_func_calls (float): number of objective function called in line search process.
a_star(Tensor): optimal step length, or 0. if the line search algorithm did not converge.
phi_star (Tensor): phi at a_star.
derphi_star (Tensor): derivative of phi at a_star.
Following summarizes the essentials of the strong Wolfe line search algorithm.
Some notations used in the description:
- `f` denotes the objective function.
- `phi` is a function of step size alpha, restricting `f` on a line.
phi = f(xk + a * pk),
where xk is the position of k'th iterate, pk is the line search direction(decent direction),
and a is the step size.
- a : substitute of alpha
- a1 is a of last iteration, which is alpha_(i-1).
- a2 is a of current iteration, which is alpha_i.
- a_lo is a in left position when calls zoom, which is alpha_low.
- a_hi is a in right position when calls zoom, which is alpha_high.
Line Search Algorithm:
repeat
Compute phi(a2) and derphi(a2).
1. If phi(a2) > phi(0) + c_1 * a2 * phi'(0) or [phi(a2) >= phi(a1) and i > 1],
a_star= zoom(a1, a2) and stop;
2. If |phi'(a2)| <= -c_2 * phi'(0),
a_star= a2 and stop;
3. If phi'(a2) >= 0,
a_star= zoom(a2, a1) and stop;
a1 = a2
a2 = min(2 * a2, a2)
i = i + 1
end(repeat)
zoom(a_lo, a_hi) Algorithm:
repeat
aj = cubic_interpolation(a_lo, a_hi)
Compute phi(aj) and derphi(aj).
1. If phi(aj) > phi(0) + c_1 * aj * phi'(0) or phi(aj) >= phi(a_lo),
then a_hi <- aj;
2.
2.1. If |phi'(aj)| <= -c_2 * phi'(0), then a_star= a2 and stop;
2.2. If phi'(aj) * (a2 - a1) >= 0, then a_hi = a_lo
a_lo = aj;
end(repeat)
"""
def phi_and_derphi(a):
r"""Compute function value and derivative of phi at a.
phi = f(xk + a * pk)
phi'(a) = f'(xk + a * pk) * pk
"""
phi_value, f_grad = _value_and_gradient(f, xk + a * pk)
phi_grad = paddle.dot(f_grad, pk)
# return f_grad to be used in bfgs/l-bfgs to compute yk to avoid computint repeatedly.
return phi_value, f_grad, phi_grad
def zoom(
a_lo,
phi_lo,
derphi_lo,
derf_lo,
a_hi,
phi_hi,
derphi_hi,
phi_0,
derphi_0,
):
# find the exact a from the bracket [a_lo, a_hi]
max_zoom_iters = max_iters
j = paddle.full(shape=[1], fill_value=0, dtype='int64')
done_zoom = paddle.full(shape=[1], fill_value=False, dtype='bool')
def cond_zoom(
j,
done_zoom,
a_lo,
phi_lo,
derphi_lo,
derf_lo,
a_hi,
phi_hi,
derphi_hi,
):
pred = paddle.abs(a_hi - a_lo) < tolerance_change
paddle.assign(done_zoom | pred, done_zoom)
return (j < max_zoom_iters) & ~done_zoom
def body_zoom(
j,
done_zoom,
a_lo,
phi_lo,
derphi_lo,
derf_lo,
a_hi,
phi_hi,
derphi_hi,
):
aj = cubic_interpolation_(
a_lo, phi_lo, derphi_lo, a_hi, phi_hi, derphi_hi
) # 21
min_change = 0.1 * paddle.abs(a_hi - a_lo)
pred = (
paddle.minimum(paddle.abs(aj - a_lo), paddle.abs(aj - a_hi))
< min_change
)
aj = paddle.static.nn.cond(
pred, lambda: 0.5 * (a_lo + a_hi), lambda: aj
)
phi_j, derf_j, derphi_j = phi_and_derphi(aj)
def true_fn():
# use assign to modify the variable in-place
paddle.assign(aj, a_hi)
paddle.assign(phi_j, phi_hi)
paddle.assign(derphi_j, derphi_hi)
def false_fn(a_lo, done_zoom):
pred3 = paddle.abs(derphi_j) <= -c2 * derphi_0
paddle.assign(pred3, done_zoom)
def true_fn():
paddle.assign(a_lo, a_hi)
paddle.assign(phi_lo, phi_hi)
paddle.assign(derphi_lo, derphi_hi)
pred4 = ~done_zoom & (derphi_j * (a_hi - a_lo) >= 0)
paddle.static.nn.cond(pred4, true_fn, None)
paddle.assign(aj, a_lo)
paddle.assign(phi_j, phi_lo)
paddle.assign(derphi_j, derphi_lo)
paddle.assign(derf_j, derf_lo)
pred2 = (phi_j > phi_0 + c1 * aj * derphi_0) | (phi_j >= phi_lo)
paddle.static.nn.cond(
pred2, true_fn, lambda: false_fn(a_lo, done_zoom)
)
j = paddle.static.nn.cond(done_zoom, lambda: j, lambda: j + 1)
return [
j,
done_zoom,
a_lo,
phi_lo,
derphi_lo,
derf_lo,
a_hi,
phi_hi,
derphi_hi,
]
paddle.static.nn.while_loop(
cond=cond_zoom,
body=body_zoom,
loop_vars=[
j,
done_zoom,
a_lo,
phi_lo,
derphi_lo,
derf_lo,
a_hi,
phi_hi,
derphi_hi,
],
)
# j is the number of object function called in zoom.
return j
alpha_max = paddle.full(shape=[1], fill_value=alpha_max, dtype=dtype)
a1 = paddle.full(shape=[1], fill_value=0.0, dtype=dtype)
a2 = paddle.full(shape=[1], fill_value=initial_step_length, dtype=dtype)
phi_1, derf_1, derphi_1 = phi_and_derphi(a1)
# use assign to cut off binding between two variables
phi_0 = paddle.assign(phi_1)
derphi_0 = paddle.assign(derphi_1)
ls_func_calls = paddle.full(shape=[1], fill_value=1, dtype='int64')
# If not found the a_star, will return alpha=0 and f(xk), derf(xk)
a_star = paddle.full(shape=[1], fill_value=0, dtype=dtype)
phi_star = paddle.assign(phi_1)
derf_star = paddle.assign(derf_1)
i = paddle.full(shape=[1], fill_value=0, dtype='int64')
done = paddle.full(shape=[1], fill_value=False, dtype='bool')
def cond(i, ls_func_calls, a1, a2, phi_1, derf_1, done):
return (i < max_iters) & ~done
def body(i, ls_func_calls, a1, a2, phi_1, derf_1, done):
phi_2, derf_2, derphi_2 = phi_and_derphi(a2)
paddle.assign(ls_func_calls + 1, ls_func_calls)
paddle.assign(done | paddle.any(paddle.isinf(phi_2)), done)
def true_fn1():
j = zoom(
a1,
phi_1,
derphi_1,
derf_1,
a2,
phi_2,
derphi_2,
phi_0,
derphi_0,
)
paddle.assign(a1, a_star)
paddle.assign(phi_1, phi_star)
paddle.assign(derf_1, derf_star)
paddle.assign(ls_func_calls + j, ls_func_calls)
pred1 = ~done & (
(phi_2 > phi_0 + c1 * a2 * derphi_0) | ((phi_2 >= phi_1) & (i > 1))
)
paddle.assign(done | pred1, done)
paddle.static.nn.cond(pred1, true_fn1, None)
def true_fn2():
paddle.assign(a2, a_star)
paddle.assign(phi_2, phi_star)
paddle.assign(derf_2, derf_star)
pred2 = ~done & (paddle.abs(derphi_2) <= -c2 * derphi_0)
paddle.assign(done | pred2, done)
paddle.static.nn.cond(pred2, true_fn2, None)
def true_fn3():
j = zoom(
a2,
phi_2,
derphi_2,
derf_2,
a1,
phi_1,
derphi_1,
phi_0,
derphi_0,
)
paddle.assign(a2, a_star)
paddle.assign(phi_2, phi_star)
paddle.assign(derf_2, derf_star)
paddle.assign(ls_func_calls + j, ls_func_calls)
pred3 = ~done & (derphi_2 >= 0)
paddle.assign(done | pred3, done)
paddle.static.nn.cond(pred3, true_fn3, None)
def false_fn():
paddle.assign(a2, a1)
paddle.assign(phi_2, phi_1)
paddle.assign(derf_2, derf_1)
paddle.assign(paddle.minimum(2 * a2, alpha_max), a2)
paddle.assign(i + 1, i)
paddle.static.nn.cond(done, None, false_fn)
return [i, ls_func_calls, a1, a2, phi_1, derf_1, done]
paddle.static.nn.while_loop(
cond=cond,
body=body,
loop_vars=[i, ls_func_calls, a1, a2, phi_1, derf_1, done],
)
return a_star, phi_star, derf_star, ls_func_calls
@@ -0,0 +1,120 @@
# 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 paddle
from paddle.base.data_feeder import check_type
from paddle.base.framework import Variable, in_pir_mode
def check_input_type(input, name, op_name):
r"""Check whether the input is tensor or variable."""
if paddle.in_dynamic_mode():
if not isinstance(input, paddle.Tensor):
raise ValueError(f"The input: {input} must be tensor.")
else:
check_type(input, name, (Variable, paddle.pir.Value), op_name)
def check_initial_inverse_hessian_estimate(H0):
r"""Check whether the specified initial_inverse_hessian_estimate is symmetric and positive definite.
Raise errors when precondition not met.
Note:
In static graph can not raise error directly, so use py_func make raise_func as a op,
and use paddle.static.nn.cond to decide if put the op in net.
cholesky is the fast way to check positive definition, but in static graph can not catch
exception to raise value error, so use eigvals rather than cholesky in static graph.
"""
is_symmetric = paddle.all(paddle.equal(H0, H0.t()))
def raise_func():
raise ValueError(
"The initial_inverse_hessian_estimate should be symmetric and positive definite, but the specified is not."
)
if paddle.in_dynamic_mode():
if not is_symmetric:
raise_func()
try:
paddle.linalg.cholesky(H0)
except RuntimeError as error:
raise_func()
elif in_pir_mode():
paddle.static.nn.control_flow.Assert(
is_symmetric,
None,
10,
name="The initial_inverse_hessian_estimate should be symmetric and positive definite, but the specified is not.",
)
eigvals = paddle.linalg.eigvals(H0)
is_positive = paddle.bitwise_and(
paddle.all(eigvals.real() > 0.0), paddle.all(eigvals.imag() == 0.0)
)
paddle.static.nn.control_flow.Assert(
is_positive,
None,
10,
name="The initial_inverse_hessian_estimate should be symmetric and positive definite, but the specified is not.",
)
else:
def create_tmp_var(program, name, dtype, shape):
return program.current_block().create_var(
name=name, dtype=dtype, shape=shape
)
out_var = create_tmp_var(
paddle.static.default_main_program(),
name='output',
dtype='float32',
shape=[-1],
)
def false_fn():
paddle.static.nn.py_func(
func=raise_func, x=is_symmetric, out=out_var
)
paddle.static.nn.cond(is_symmetric, None, false_fn)
# eigvals only support cpu
paddle.set_device("cpu")
eigvals = paddle.linalg.eigvals(H0)
is_positive = paddle.all(eigvals.real() > 0.0) and paddle.all(
eigvals.imag() == 0.0
)
paddle.static.nn.cond(is_positive, None, false_fn)
def _value_and_gradient(f, x, v=None):
r"""Compute function value and gradient of f at x.
Args:
f (Callable): the objective function.
x (Tensor): the input tensor.
Returns:
value: a tensor that holds the function value.
gradient: a tensor that holds the function gradients.
"""
# use detach to cut off relation between x and original graph
x = x.detach()
x.stop_gradient = False
value = f(x)
if paddle.in_dynamic_mode():
# only need to compute first order derivative, and some op dont support high order derivative.
gradient = paddle.grad([value], [x], create_graph=False)[0]
else:
gradient = paddle.static.gradients([value], [x])[0]
# use detach to make results real number without grad to avoid assign error
return value.detach(), gradient.detach()
@@ -0,0 +1,383 @@
# Copyright (c) 2019 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 paddle
from paddle.base import core
from paddle.base.framework import (
Variable,
default_main_program,
default_startup_program,
device_guard,
in_dygraph_mode,
program_guard,
)
__all__ = []
class GradientMergeOptimizer:
"""
Gradient Merge, also called as Gradient Accumulation,
is a training strategy for larger batches. With this strategy,
the parameter will not be updated until specific steps.
For each step, the forward network and the backward network
will run to calculate the gradient of the parameters.
For every k step, the optimization network will run,
applying a specific optimization method (such as SGD, Adam)
to the parameters.
Args:
inner_optimizer (Optimizer): The specific optimization (such as SGD, Adam)
which update the parameters
k_steps (int): the update period of the parameters
avg (bool): whether to average the gradients of each mini-batch,
the default value is `True`
Examples:
.. code-block:: pycon
>>> import paddle
>>> import numpy as np
>>> paddle.enable_static()
>>> def gen_data(batch_size):
... return {
... "x": np.random.random(size=(batch_size, 32)).astype('float32'),
... "y": np.random.random(size=(batch_size, 1)).astype('int64'),
... }
>>> def mlp(input_x, input_y, hid_dim=128, label_dim=2):
... fc_1 = paddle.static.nn.fc(x=input_x, size=hid_dim)
... prediction = paddle.static.nn.fc(x=[fc_1], size=label_dim, activation='softmax')
... cost = paddle.nn.functional.cross_entropy(
... input=prediction,
... label=input_y,
... reduction='none',
... use_softmax=False,
... )
... sum_cost = paddle.mean(cost)
... return sum_cost, fc_1, prediction
>>> input_x = paddle.static.data(name="x", shape=[-1, 32], dtype='float32')
>>> input_y = paddle.static.data(name="y", shape=[-1, 1], dtype='int64')
>>> cost, fc_1, pred = mlp(input_x, input_y)
>>> sgd = paddle.optimizer.Adam(learning_rate=0.01)
>>> sgd = paddle.incubate.optimizer.GradientMergeOptimizer(sgd, k_steps=4, avg=True)
>>> sgd.minimize(cost)
>>> place = paddle.CPUPlace()
>>> exe = paddle.static.Executor(place)
>>> exe.run(paddle.static.default_startup_program())
>>> for i in range(10):
... cost_val = exe.run(
... feed=gen_data(32),
... program=paddle.static.default_main_program(),
... fetch_list=[cost.name],
... )
... print("step=%d, cost=%f" % (i, cost_val[0]))
"""
GRAD_MERGE_COND_NAME = "grad_merge_cond_name"
def __init__(self, inner_optimizer, k_steps=1, avg=True):
if in_dygraph_mode():
raise Exception(
"In dygraph, we don't support GradientMergeOptimizer."
"You can do Gradient merge by yourself with k-times forward + backward, "
"and one-time optimizer.minimize()"
)
assert inner_optimizer is not None, "inner optimizer can not be None"
assert isinstance(k_steps, int) and k_steps > 0, (
"k_steps should be a positive integer"
)
self.inner_optimizer = inner_optimizer
self.k_steps = k_steps
self.type = "gradient_merge"
self.avg = avg
self._optimize_ops = None
def _set_k_steps(self, k_steps):
self.k_steps = k_steps
def _set_avg(self, avg):
self.avg = avg
def backward(
self,
loss,
startup_program=None,
parameter_list=None,
no_grad_set=None,
callbacks=None,
):
assert isinstance(loss, Variable), "The loss should be an Variable."
assert parameter_list is None, (
"The parameter_list should be None when using GradientMergeOptimizer"
)
assert no_grad_set is None, (
"The no_grad_set should be None when using GradientMergeOptimizer"
)
params_grads = self.inner_optimizer.backward(
loss, startup_program=startup_program
)
return params_grads
def apply_optimize(self, loss, startup_program, params_grads):
program = loss.block.program
with program_guard(program, startup_program):
optimize_ops = self.apply_gradients(params_grads)
return optimize_ops
def _is_the_backward_op(self, op):
op_maker = core.op_proto_and_checker_maker
backward = core.op_proto_and_checker_maker.OpRole.Backward
if op_maker.kOpRoleVarAttrName() in op.attr_names and int(
op.all_attrs()[op_maker.kOpRoleAttrName()]
) == int(backward):
return True
return False
def _remove_op_role_var(self, param, grad):
op_maker = core.op_proto_and_checker_maker
op = grad.op
assert self._is_the_backward_op(op), (
f'grad.op={op} is not the backward op which produces the grad={grad.name}'
)
block = grad.block
var_attr = op.all_attrs()[op_maker.kOpRoleVarAttrName()]
assert param.name in var_attr, (
f'when using GradientMergeOptimizer, param={param.name} must be in var_attr={var_attr}'
)
assert grad.name in var_attr, (
f'when using GradientMergeOptimizer, grad={param.name} must be in var_attr={var_attr}'
)
# remove (param, grad) from op_role_var
var_attr.remove(param.name)
var_attr.remove(grad.name)
if len(var_attr) > 1:
op._set_attr(op_maker.kOpRoleVarAttrName(), var_attr)
else:
op._remove_attr(op_maker.kOpRoleVarAttrName())
def _add_gm_op_role_var(self, op, param, grad, cond):
grad.op = op
op_maker = core.op_proto_and_checker_maker
backward = op_maker.OpRole.Backward
# NOTE(wangxi). When distributed, we will insert grad_merge_all_reduce_op_handle
# in multi_devices_graph_pass, which will allreduce(grad) if cond is True, else
# do nothing.
# In this way, the gradient can be merged first, and then communicate when the
# condition is met, reducing the number of communications to increase the
# speed.
op._set_attr(self.GRAD_MERGE_COND_NAME, cond.name)
op._set_attr(op_maker.kOpRoleAttrName(), backward)
op._set_attr(op_maker.kOpRoleVarAttrName(), [param.name, grad.name])
def _get_gm_cond_var(self, main_block):
# Add const var
k_step_var = paddle.static.create_global_var(
name="gradient_merge_k",
shape=[1],
value=int(self.k_steps),
dtype='int32',
persistable=True,
force_cpu=True,
)
zero_var = paddle.static.create_global_var(
name="gradient_merge_zero",
shape=[1],
value=0,
dtype='int32',
persistable=True,
force_cpu=True,
)
# Add step var & cond var
step_var = paddle.static.create_global_var(
name="gradient_merge_step",
shape=[1],
value=0,
dtype='int32',
persistable=True,
force_cpu=True,
)
cond_var = main_block.create_var(
name="gradient_merge_cond", shape=[1], dtype='bool'
)
with device_guard("cpu"):
# step_var = (step_var + 1) % k_step
paddle.increment(x=step_var, value=1.0)
main_block.append_op(
type='elementwise_mod',
inputs={'X': step_var, 'Y': k_step_var},
outputs={'Out': step_var},
attrs={'axis': -1},
)
# cond_var = (step_var == 0)
main_block.append_op(
type='equal',
inputs={'X': step_var, 'Y': zero_var},
outputs={'Out': cond_var},
)
return cond_var
def apply_gradients(self, params_grads):
main_program = default_main_program()
startup_program = default_startup_program()
main_block = main_program.global_block()
startup_block = startup_program.global_block()
cond = self._get_gm_cond_var(main_block)
# TODO(mapingshuo) support sparse embedding
# step1: remove grad.op's op_role_var
for param, grad in params_grads:
assert param.type != core.VarDesc.VarType.SELECTED_ROWS, (
"SELECTED_ROWS is not supported in GradientMergeOptimizer for now"
)
self._remove_op_role_var(param, grad)
param_to_grad = {k.name: v for (k, v) in params_grads}
param_names = param_to_grad.keys()
param_to_gradient_merge = {}
new_params_grads = []
# step2: create gradient_merge var and init with 0
# and update op_role_var
for param, grad in params_grads:
param_name = param.name
param_var = main_block.var(param_name)
assert param_var is not None
gradient_merge_var = main_block.create_var(
name=param_name + "@GRAD@GradientMerge",
shape=param_var.shape,
dtype=param_var.dtype,
persistable=True,
)
param_to_gradient_merge[param_name] = gradient_merge_var
startup_gradient_merge_var = startup_block.create_var(
name=param_name + "@GRAD@GradientMerge",
shape=param_var.shape,
dtype=param_var.dtype,
persistable=True,
)
startup_block.append_op(
type="fill_constant",
outputs={"Out": startup_gradient_merge_var},
attrs={
"shape": param_var.shape,
"dtype": param_var.dtype,
"value": float(0),
},
)
# grad_merge += grad
new_grad_op = main_block.append_op(
type="elementwise_add",
inputs={'X': grad, 'Y': gradient_merge_var},
outputs={'Out': gradient_merge_var},
attrs={'axis': -1},
)
self._add_gm_op_role_var(
new_grad_op, param, gradient_merge_var, cond
)
new_params_grads.append([param, gradient_merge_var])
def true_apply_gradient():
cur_block_idx = main_program.current_block_idx
cur_block = main_program.current_block()
# cur_block's forward_block & backward_block is itself
cur_block._set_forward_block_idx(cur_block_idx)
op_maker = core.op_proto_and_checker_maker
if self.avg:
for param, new_grad in new_params_grads:
# grad /= k_steps
cur_block.append_op(
type='scale',
inputs={'X': new_grad},
outputs={'Out': new_grad},
attrs={
'scale': 1.0 / self.k_steps,
'bias': 0.0,
'bias_after_scale': False,
},
)
new_grad.op._set_attr(
op_maker.kOpRoleAttrName(), op_maker.OpRole.Backward
)
for param, new_grad in new_params_grads:
# NOTE. regularization will append ops to grad.block,
# while new_grad's real block is global_block,
# but we want append regularization ops to cur_block,
# so we set new_grad.block = cur_block
new_grad.block = cur_block
self._optimize_ops = self.inner_optimizer.apply_gradients(
new_params_grads
)
# clear gradient_merge_vars
for param, new_grad in new_params_grads:
paddle.tensor.fill_constant(
shape=new_grad.shape,
dtype=new_grad.dtype,
value=0.0,
out=new_grad,
)
new_grad.op._set_attr(
op_maker.kOpRoleAttrName(), op_maker.OpRole.Optimize
)
# step3. apply gradient
paddle.static.nn.cond(cond, true_fn=true_apply_gradient, false_fn=None)
return self._optimize_ops
def minimize(
self, loss, startup_program=None, parameter_list=None, no_grad_set=None
):
assert isinstance(loss, Variable), "The loss should be an Variable."
params_grads = self.backward(
loss,
startup_program=startup_program,
parameter_list=parameter_list,
no_grad_set=no_grad_set,
)
optimize_ops = self.apply_optimize(
loss, startup_program=startup_program, params_grads=params_grads
)
return optimize_ops, params_grads
@@ -0,0 +1,240 @@
# Copyright (c) 2019 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 warnings
from paddle import _C_ops, _legacy_C_ops, pir
from paddle.base import framework
from paddle.framework import (
in_dynamic_mode,
in_pir_mode,
)
from paddle.optimizer import Optimizer
class LarsMomentumOptimizer(Optimizer):
r"""
Momentum optimizer with LARS support
The update equations are as follows:
.. math::
& local\_learning\_rate = learning\_rate * lars\_coeff * \\
\\frac{||param||}{||gradient|| + lars\_weight\_decay * ||param||}
& velocity = mu * velocity + local\_learning\_rate * (gradient + lars\_weight\_decay * param + epsilon)
& param = param - velocity
Parameters:
learning_rate (float|Variable): The learning rate used to update parameters. \
Can be a float value or a Variable with one float value as data element. \
momentum (float): momentum factor
lars_coeff (float): Defines how much we trust the layer to change its weights.
lars_weight_decay (float): Weight decay coefficient for decaying using LARS.
parameter_list (Iterable, optional): Iterable of ``Variable`` names to update to minimize ``loss``. \
This parameter is required in dygraph mode. \
The default value is None in static graph mode, at this time all parameters will be updated.
regularization (WeightDecayRegularizer, optional): The strategy of regularization. There are two method: \
:ref:`api_paddle_regularizer_L1Decay` , :ref:`api_paddle_regularizer_L2Decay` . If a parameter has set \
regularizer using :ref:`api_paddle_ParamAttr` already, the regularization setting here in optimizer will be \
ignored for this parameter. Otherwise, the regularization setting here in optimizer will take effect. \
Default None, meaning there is no regularization.
grad_clip (GradientClipBase, optional): Gradient clipping strategy, it's an instance of
some derived class of ``GradientClipBase`` . There are three clipping strategies
( :ref:`api_paddle_nn_ClipGradByGlobalNorm` , :ref:`api_paddle_nn_ClipGradByNorm` ,
:ref:`api_paddle_nn_ClipGradByValue` ). Default None, meaning there is no gradient clipping.
name (str, optional): This parameter is used by developers to print debugging information. \
For details, please refer to :ref:`api_guide_Name`. Default is None.
exclude_from_weight_decay (list[str], optional): Name string of layers which will be exclude from lars weight decay. Default is None.
epsilon (float, optional): Epsilon to avoid Division by Zero when calculate local lr. Default is 0.
multi_precision (bool, optional): Whether to use multi-precision during weight updating.
rescale_grad (float, optional): Multiply the gradient with `rescale_grad` \
before updating. Often choose to be `1.0/batch_size`.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import numpy as np
>>> paddle.enable_static()
>>> np_inp = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32)
>>> inp = paddle.static.data(
... name="inp", shape=[2, 2], dtype='float32')
>>> out = paddle.static.nn.fc(inp, size=3)
>>> out = paddle.sum(out)
>>> optimizer = paddle.incubate.optimizer.LarsMomentumOptimizer(learning_rate=0.001, momentum=0.9)
>>> optimizer.minimize(out)
>>> exe = paddle.static.Executor(paddle.CPUPlace())
>>> exe.run(paddle.static.default_startup_program())
>>> exe.run(
... feed={"inp": np_inp},
... fetch_list=[out.name])
"""
_velocity_acc_str = "velocity"
def __init__(
self,
learning_rate,
momentum,
lars_coeff=0.001,
lars_weight_decay=0.0005,
parameter_list=None,
regularization=None,
grad_clip=None,
name=None,
exclude_from_weight_decay=None,
epsilon=0,
multi_precision=False,
rescale_grad=1.0,
):
assert learning_rate is not None
assert momentum is not None
super().__init__(
learning_rate=learning_rate,
parameters=parameter_list,
weight_decay=regularization,
grad_clip=grad_clip,
name=name,
)
self.type = "lars_momentum"
self._momentum = momentum
self._lars_coeff = float(lars_coeff)
self._lars_weight_decay = float(lars_weight_decay)
self._epsilon = float(epsilon)
if exclude_from_weight_decay is None:
self._exclude_from_weight_decay = []
else:
self._exclude_from_weight_decay = exclude_from_weight_decay
self._multi_precision = multi_precision
self._rescale_grad = float(rescale_grad)
self._master_weights = {}
def _create_accumulators(self, block, parameters):
if not isinstance(block, (framework.Block, pir.Block)):
raise TypeError("block is not instance of Block.")
for p in parameters:
if self._multi_precision and self._is_dtype_fp16_or_bf16(p.dtype):
master_p = self._create_master_weight(p)
self._add_accumulator(self._velocity_acc_str, master_p)
continue
if (
self._is_dtype_fp16_or_bf16(p.dtype)
and not self._multi_precision
):
warnings.warn(
"Accumulating with FP16/BF16 in optimizer can lead to poor accuracy or slow convergence."
"Consider using multi_precision=True option of the Lars optimizer."
)
self._add_accumulator(self._velocity_acc_str, p)
def _append_optimize_op(self, block, param_and_grad):
if not isinstance(block, (framework.Block, pir.Block)):
raise TypeError("block is not instance of Block.")
_lars_weight_decay = self._lars_weight_decay
param_name = param_and_grad[0].name
if len(self._exclude_from_weight_decay) > 0:
for name in self._exclude_from_weight_decay:
if name in param_name:
_lars_weight_decay = 0.0
break
velocity_acc = self._get_accumulator_master(
self._velocity_acc_str, param_and_grad[0]
)
lr = self._create_param_lr(param_and_grad)
find_master = self._multi_precision and self._is_dtype_fp16_or_bf16(
param_and_grad[0].dtype
)
master_weight = (
self._master_weights[param_and_grad[0].name]
if find_master
else None
)
attrs = {
"mu": self._momentum,
"lars_coeff": self._lars_coeff,
"lars_weight_decay": [_lars_weight_decay],
"multi_precision": find_master,
"epsilon": self._epsilon,
"rescale_grad": self._rescale_grad,
}
inputs = {
"Param": param_and_grad[0],
"Grad": param_and_grad[1],
"Velocity": velocity_acc,
"LearningRate": lr,
}
outputs = {"ParamOut": param_and_grad[0], "VelocityOut": velocity_acc}
if find_master:
inputs["MasterParam"] = master_weight
outputs["MasterParamOut"] = master_weight
if in_dynamic_mode():
tmp, tmp2 = _legacy_C_ops.lars_momentum(
[param_and_grad[0]],
[param_and_grad[1]],
[velocity_acc],
[lr],
[param_and_grad[0]],
[velocity_acc],
"mu",
self._momentum,
"lars_coeff",
self._lars_coeff,
"lars_weight_decay",
[_lars_weight_decay],
"multi_precision",
find_master,
"epsilon",
self._epsilon,
"rescale_grad",
self._rescale_grad,
)
elif in_pir_mode():
if isinstance(master_weight, pir.Value):
master_weight = [master_weight]
_, _, _ = _C_ops.lars_momentum_(
[param_and_grad[0]],
[param_and_grad[1]],
[velocity_acc],
[lr],
master_weight,
self._momentum,
self._lars_coeff,
[_lars_weight_decay],
self._epsilon,
find_master,
self._rescale_grad,
)
return None
else:
# create the momentum optimize op
momentum_op = block.append_op(
type=self.type,
inputs=inputs,
outputs=outputs,
attrs=attrs,
stop_gradient=True,
)
return momentum_op
+440
View File
@@ -0,0 +1,440 @@
# 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 __future__ import annotations
from collections import defaultdict
from functools import reduce
from typing import TYPE_CHECKING, Any, Literal, TypeVar
import paddle
from paddle.optimizer import Optimizer
from paddle.utils import deprecated
from .line_search_dygraph import _strong_wolfe
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from paddle import Tensor
from paddle.nn.clip import GradientClipBase
from paddle.optimizer.optimizer import _ParameterConfig
from paddle.regularizer import WeightDecayRegularizer
_T_co = TypeVar('_T_co', covariant=True)
@deprecated(since="2.5.0", update_to="paddle.optimizer.LBFGS", level=1)
class LBFGS(Optimizer):
r"""
The L-BFGS is a quasi-Newton method for solving an unconstrained optimization problem over a differentiable function.
Closely related is the Newton method for minimization. Consider the iterate update formula:
.. math::
x_{k+1} = x_{k} + H_k \nabla{f_k}
If :math:`H_k` is the inverse Hessian of :math:`f` at :math:`x_k`, then it's the Newton method.
If :math:`H_k` is symmetric and positive definite, used as an approximation of the inverse Hessian, then
it's a quasi-Newton. In practice, the approximated Hessians are obtained
by only using the gradients, over either whole or part of the search
history, the former is BFGS, the latter is L-BFGS.
Reference:
Jorge Nocedal, Stephen J. Wright, Numerical Optimization, Second Edition, 2006. pp179: Algorithm 7.5 (L-BFGS).
Args:
learning_rate (float, optional): learning rate .The default value is 1.
max_iter (int, optional): maximal number of iterations per optimization step.
The default value is 20.
max_eval (int, optional): maximal number of function evaluations per optimization
step. The default value is max_iter * 1.25.
tolerance_grad (float, optional): termination tolerance on first order optimality
The default value is 1e-5.
tolerance_change (float, optional): termination tolerance on function
value/parameter changes. The default value is 1e-9.
history_size (int, optional): update history size. The default value is 100.
line_search_fn (string, optional): either 'strong_wolfe' or None. The default value is strong_wolfe.
parameters (list|tuple, optional): List/Tuple of ``Tensor`` names to update to minimize ``loss``. \
This parameter is required in dygraph mode. The default value is None.
weight_decay (float|WeightDecayRegularizer, optional): The strategy of regularization. \
It canbe a float value as coeff of L2 regularization or \
:ref:`api_paddle_regularizer_L1Decay`, :ref:`api_paddle_regularizer_L2Decay`.
If a parameter has set regularizer using :ref:`api_paddle_ParamAttr` already, \
the regularization setting here in optimizer will be ignored for this parameter. \
Otherwise, the regularization setting here in optimizer will take effect. \
Default None, meaning there is no regularization.
grad_clip (GradientClipBase, optional): Gradient clipping strategy, it's an instance of \
some derived class of ``GradientClipBase`` . There are three clipping strategies \
( :ref:`api_paddle_nn_ClipGradByGlobalNorm` , :ref:`api_paddle_nn_ClipGradByNorm` , \
:ref:`api_paddle_nn_ClipGradByValue` ). Default None, meaning there is no gradient clipping.
name (str, optional): Normally there is no need for user to set this property.
For more information, please refer to :ref:`api_guide_Name`.
The default value is None.
Return:
loss (Tensor): the final loss of closure.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import numpy as np
>>> from paddle.incubate.optimizer import LBFGS
>>> paddle.disable_static()
>>> np.random.seed(0)
>>> np_w = np.random.rand(1).astype(np.float32)
>>> np_x = np.random.rand(1).astype(np.float32)
>>> inputs = [np.random.rand(1).astype(np.float32) for i in range(10)]
>>> # y = 2x
>>> targets = [2 * x for x in inputs]
>>> class Net(paddle.nn.Layer):
... def __init__(self):
... super().__init__()
... w = paddle.to_tensor(np_w)
... self.w = paddle.create_parameter(shape=w.shape, dtype=w.dtype, default_initializer=paddle.nn.initializer.Assign(w))
... def forward(self, x):
... return self.w * x
>>> net = Net()
>>> opt = LBFGS(learning_rate=1, max_iter=1, max_eval=None, tolerance_grad=1e-07, tolerance_change=1e-09, history_size=100, line_search_fn='strong_wolfe', parameters=net.parameters())
>>> def train_step(inputs, targets):
... def closure():
... outputs = net(inputs)
... loss = paddle.nn.functional.mse_loss(outputs, targets)
... print('loss: ', loss.item())
... opt.clear_grad()
... loss.backward()
... return loss
... opt.step(closure)
>>> for input, target in zip(inputs, targets):
... input_tensor = paddle.to_tensor(input)
... target_tensor = paddle.to_tensor(target)
... train_step(input_tensor, target_tensor)
"""
learning_rate: float
max_iter: int
max_eval: int
tolerance_grad: float
tolerance_change: float
history_size: int
line_search_fn: Literal['strong_wolfe'] | None
state: dict[str, dict[str, Any]]
def __init__(
self,
learning_rate: float = 1.0,
max_iter: int = 20,
max_eval: int | None = None,
tolerance_grad: float = 1e-7,
tolerance_change: float = 1e-9,
history_size: int = 100,
line_search_fn: Literal['strong_wolfe'] | None = None,
parameters: Sequence[Tensor] | Sequence[_ParameterConfig] | None = None,
weight_decay: float | WeightDecayRegularizer | None = None,
grad_clip: GradientClipBase | None = None,
name: str | None = None,
) -> Tensor:
if max_eval is None:
max_eval = max_iter * 5 // 4
self.learning_rate = learning_rate
self.max_iter = max_iter
self.max_eval = max_eval
self.tolerance_grad = tolerance_grad
self.tolerance_change = tolerance_change
self.history_size = history_size
self.line_search_fn = line_search_fn
if isinstance(parameters, paddle.Tensor):
raise TypeError(
"parameters argument given to the optimizer should be "
"an iterable of Tensors or dicts, but got " + type(parameters)
)
self.state = defaultdict(dict)
super().__init__(
learning_rate=1.0,
parameters=parameters,
weight_decay=weight_decay,
grad_clip=grad_clip,
name=name,
)
if not isinstance(self._parameter_list[0], dict):
self._params = self._parameter_list
else:
for idx, param_group in enumerate(self._param_groups):
self._params = param_group['params']
self._numel_cache = None
def state_dict(self) -> dict[str, dict[str, Any]]:
r"""Returns the state of the optimizer as a :class:`dict`.
Return:
state, a dict holding current optimization state. Its content
differs between optimizer classes.
"""
packed_state = {}
for k, v in self.state.items():
packed_state.update({k: v})
return {'state': packed_state}
def _numel(self):
# compute the number of all parameters
if self._numel_cache is None:
self._numel_cache = reduce(
lambda total, p: total + p.numel(), self._params, 0
)
return self._numel_cache
# flatten grad of all parameters
def _gather_flat_grad(self):
views = []
for p in self._params:
if p.grad is None:
view = paddle.zeros_like(p).reshape([-1])
else:
view = p.grad.reshape([-1])
views.append(view)
return paddle.concat(views, axis=0)
# compute xk = xk + alpha * direction
def _add_grad(self, alpha, direction):
offset = 0
for p in self._params:
numel = reduce(lambda x, y: x * y, p.shape)
p = paddle.assign(
p.add(
direction[offset : offset + numel].reshape(p.shape) * alpha
),
p,
)
offset += numel
assert offset == self._numel()
def _clone_param(self):
return [p.clone() for p in self._params]
def _set_param(self, params_data):
for p, pdata in zip(self._params, params_data):
paddle.assign(pdata, p)
def _directional_evaluate(self, closure, x, alpha, d):
self._add_grad(alpha, d)
loss = float(closure())
flat_grad = self._gather_flat_grad()
self._set_param(x)
return loss, flat_grad
def step(self, closure: Callable[[], _T_co]) -> _T_co:
"""
Performs a single optimization step.
Args:
closure (callable): A closure that reevaluates the model
and returns the loss.
"""
with paddle.no_grad():
# Make sure the closure is always called with grad enabled
closure = paddle.enable_grad()(closure)
learning_rate = self.learning_rate
max_iter = self.max_iter
max_eval = self.max_eval
tolerance_grad = self.tolerance_grad
tolerance_change = self.tolerance_change
line_search_fn = self.line_search_fn
history_size = self.history_size
state = self.state
state.setdefault('func_evals', 0)
state.setdefault('n_iter', 0)
# evaluate initial f(x) and df/dx
orig_loss = closure()
loss = float(orig_loss)
current_evals = 1
state['func_evals'] += 1
flat_grad = self._gather_flat_grad()
opt_cond = flat_grad.abs().max() <= tolerance_grad
# optimal condition
if opt_cond:
return orig_loss
# tensors cached in state (for tracing)
d = state.get('d')
alpha = state.get('alpha')
old_yk = state.get('old_yk')
old_sk = state.get('old_sk')
ro = state.get('ro')
H_diag = state.get('H_diag')
prev_flat_grad = state.get('prev_flat_grad')
prev_loss = state.get('prev_loss')
n_iter = 0
# optimize for a max of max_iter iterations
while n_iter < max_iter:
# keep track of nb of iterations
n_iter += 1
state['n_iter'] += 1
############################################################
# compute gradient descent direction
############################################################
if state['n_iter'] == 1:
d = flat_grad.neg()
old_yk = []
old_sk = []
ro = []
H_diag = paddle.to_tensor(1.0, dtype=orig_loss.dtype)
else:
# do lbfgs update (update memory)
y = flat_grad.subtract(prev_flat_grad)
s = d.multiply(paddle.to_tensor(alpha, dtype=d.dtype))
ys = y.dot(s)
if ys > 1e-10:
# updating memory
if len(old_yk) == history_size:
# shift history by one (limited-memory)
old_yk.pop(0)
old_sk.pop(0)
ro.pop(0)
# store new direction/step
old_yk.append(y)
old_sk.append(s)
ro.append(1.0 / ys)
# update scale of initial Hessian approximation
H_diag = ys / y.dot(y) # (y*y)
# compute the approximate (L-BFGS) inverse Hessian
# multiplied by the gradient
num_old = len(old_yk)
if 'al' not in state:
state['al'] = [None] * history_size
al = state['al']
# iteration in L-BFGS loop collapsed to use just one buffer
q = flat_grad.neg()
for i in range(num_old - 1, -1, -1):
al[i] = old_sk[i].dot(q) * ro[i]
paddle.assign(q.add(old_yk[i] * (-al[i])), q)
# multiply by initial Hessian
# r/d is the final direction
d = r = paddle.multiply(q, H_diag)
for i in range(num_old):
be_i = old_yk[i].dot(r) * ro[i]
paddle.assign(r.add(old_sk[i] * (al[i] - be_i)), r)
if prev_flat_grad is None:
prev_flat_grad = flat_grad.clone()
else:
paddle.assign(flat_grad, prev_flat_grad)
prev_loss = loss
############################################################
# compute step length
############################################################
# reset initial guess for step size
if state['n_iter'] == 1:
alpha = (
min(1.0, 1.0 / flat_grad.abs().sum()) * learning_rate
)
else:
alpha = learning_rate
# directional derivative
gtd = flat_grad.dot(d)
# directional derivative is below tolerance
if gtd > -tolerance_change:
break
# optional line search: user function
ls_func_evals = 0
if line_search_fn is not None:
# perform line search, using user function
if line_search_fn != "strong_wolfe":
raise RuntimeError("only 'strong_wolfe' is supported")
else:
x_init = self._clone_param()
def obj_func(x, alpha, d):
return self._directional_evaluate(
closure, x, alpha, d
)
loss, flat_grad, alpha, ls_func_evals = _strong_wolfe(
obj_func, x_init, alpha, d, loss, flat_grad, gtd
)
self._add_grad(alpha, d)
opt_cond = flat_grad.abs().max() <= tolerance_grad
else:
# no line search, simply move with fixed-step
self._add_grad(alpha, d)
if n_iter != max_iter:
with paddle.enable_grad():
loss = float(closure())
flat_grad = self._gather_flat_grad()
opt_cond = flat_grad.abs().max() <= tolerance_grad
ls_func_evals = 1
# update func eval
current_evals += ls_func_evals
state['func_evals'] += ls_func_evals
# optimal condition
if opt_cond:
break
# lack of progress
if (d * alpha).abs().max() <= tolerance_change:
break
if abs(loss - prev_loss) < tolerance_change:
break
# check conditions
if current_evals >= max_eval:
break
if n_iter == max_iter:
break
state['d'] = d
state['alpha'] = alpha
state['old_yk'] = old_yk
state['old_sk'] = old_sk
state['ro'] = ro
state['H_diag'] = H_diag
state['prev_flat_grad'] = prev_flat_grad
state['prev_loss'] = prev_loss
return orig_loss
@@ -0,0 +1,297 @@
# 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 paddle
def _cubic_interpolate(x1, f1, g1, x2, f2, g2, bounds=None):
r"""Cubic interpolation between (x1, f1, g1) and (x2, f2, g2).
Use two points and their gradient to determine a cubic function and get the minimum point
between them in the cubic curve.
Reference:
Jorge Nocedal, Stephen J. Wright, Numerical Optimization, Second Edition, 2006.
pp59: formula 3.59
Args:
x1, f1, g1: point1's position, value and gradient.
x2, f2, g2: point2's position, value and gradient.
bounds: bounds of interpolation area
Returns:
min_pos: the minimum point between the specified points in the cubic curve.
"""
# Compute bounds of interpolation area
if bounds is not None:
xmin_bound, xmax_bound = bounds
else:
xmin_bound, xmax_bound = (x1, x2) if x1 <= x2 else (x2, x1)
d1 = g1 + g2 - 3 * (f1 - f2) / (x1 - x2)
d2_square = d1**2 - g1 * g2
if d2_square >= 0:
d2 = d2_square.sqrt()
if x1 <= x2:
min_pos = x2 - (x2 - x1) * ((g2 + d2 - d1) / (g2 - g1 + 2 * d2))
else:
min_pos = x1 - (x1 - x2) * ((g1 + d2 - d1) / (g1 - g2 + 2 * d2))
return min(max(min_pos, xmin_bound), xmax_bound)
else:
return (xmin_bound + xmax_bound) / 2.0
def _strong_wolfe(
obj_func,
xk,
alpha,
d,
loss,
grad,
gtd,
c1=1e-4,
c2=0.9,
tolerance_change=1e-9,
max_ls=25,
):
r"""Implements of line search algorithm that satisfies the strong Wolfe conditions using double zoom.
Reference:
Jorge Nocedal, Stephen J. Wright, Numerical Optimization, Second Edition, 2006.
pp60: Algorithm 3.5 (Line Search Algorithm).
Args:
obj_func: the objective function to minimize. ```` accepts a multivariate input and returns a scalar.
xk (Tensor): the starting point of the iterates.
alpha (Scalar): the initial step size.
d (Tensor): search direction.
loss (scalar): the initial loss
grad (Tensor): the initial grad
c1 (Scalar): parameter for sufficient decrease condition.
c2 (Scalar): parameter for curvature condition.
tolerance_change (Scalar): terminates if the change of function value/position/parameter between
two iterations is smaller than this value.
max_ls(int): max iteration of line search.
alpha_max (float): max step length.
Returns:
loss_new (Scaler): loss of obj_func at final alpha.
grad_new, (Tensor): derivative of obj_func at final alpha.
alpha(Tensor): optimal step length, or 0. if the line search algorithm did not converge.
ls_func_evals (Scaler): number of objective function called in line search process.
Following summarizes the essentials of the strong Wolfe line search algorithm.
Some notations used in the description:
- `func` denotes the objective function.
- `obi_func` is a function of step size alpha, restricting `obj_func` on a line.
obi_func = func(xk + alpha * d),
where xk is the position of k'th iterate, d is the line search direction(decent direction),
and a is the step size.
- alpha : substitute of alpha
- a1 is alpha of last iteration, which is alpha_(i-1).
- a2 is alpha of current iteration, which is alpha_i.
- a_lo is alpha in left position when calls zoom, which is alpha_low.
- a_hi is alpha in right position when calls zoom, which is alpha_high.
Line Search Algorithm:
repeat
Compute obi_func(a2) and derphi(a2).
1. If obi_func(a2) > obi_func(0) + c_1 * a2 * obi_func'(0) or [obi_func(a2) >= obi_func(a1) and i > 1],
alpha= zoom(a1, a2) and stop;
2. If |obi_func'(a2)| <= -c_2 * obi_func'(0),
alpha= a2 and stop;
3. If obi_func'(a2) >= 0,
alpha= zoom(a2, a1) and stop;
a1 = a2
a2 = min(2 * a2, a2)
i = i + 1
end(repeat)
zoom(a_lo, a_hi) Algorithm:
repeat
aj = cubic_interpolation(a_lo, a_hi)
Compute obi_func(aj) and derphi(aj).
1. If obi_func(aj) > obi_func(0) + c_1 * aj * obi_func'(0) or obi_func(aj) >= obi_func(a_lo),
then a_hi <- aj;
2.
2.1. If |obi_func'(aj)| <= -c_2 * obi_func'(0), then alpha= a2 and stop;
2.2. If obi_func'(aj) * (a2 - a1) >= 0, then a_hi = a_lo
a_lo = aj;
end(repeat)
"""
d_norm = d.abs().max()
grad = grad.clone()
# evaluate objective and gradient using initial step
loss_new, grad_new = obj_func(xk, alpha, d)
ls_func_evals = 1
gtd_new = paddle.dot(grad_new, d)
# bracket an interval containing a point satisfying the Wolfe criteria
t_prev, f_prev, g_prev, gtd_prev = (
paddle.to_tensor(0, dtype=grad.dtype),
loss,
grad,
gtd,
)
done = False
ls_iter = 0
while ls_iter < max_ls:
# check conditions
if loss_new > (loss + c1 * alpha * gtd) or (
ls_iter > 1 and loss_new >= f_prev
):
bracket = [t_prev, alpha]
bracket_f = [f_prev, loss_new]
bracket_g = [g_prev, grad_new.clone()]
bracket_gtd = [gtd_prev, gtd_new]
break
if paddle.abs(gtd_new) <= -c2 * gtd:
bracket = [alpha]
bracket_f = [loss_new]
bracket_g = [grad_new]
done = True
break
if gtd_new >= 0:
bracket = [t_prev, alpha]
bracket_f = [f_prev, loss_new]
bracket_g = [g_prev, grad_new.clone()]
bracket_gtd = [gtd_prev, gtd_new]
break
# interpolate
min_step = alpha + 0.01 * (alpha - t_prev)
max_step = alpha * 10
tmp = alpha
alpha = _cubic_interpolate(
t_prev,
f_prev,
gtd_prev,
alpha,
loss_new,
gtd_new,
bounds=(min_step, max_step),
)
# next step
t_prev = tmp
f_prev = loss_new
g_prev = grad_new.clone()
gtd_prev = gtd_new
loss_new, grad_new = obj_func(xk, alpha, d)
ls_func_evals += 1
gtd_new = grad_new.dot(d)
ls_iter += 1
# reached max number of iterations?
if ls_iter == max_ls:
bracket = [0, alpha]
bracket_f = [loss, loss_new]
bracket_g = [grad, grad_new]
# zoom phase: we now have a point satisfying the criteria, or
# a bracket around it. We refine the bracket until we find the
# exact point satisfying the criteria
insuf_progress = False
# find high and low points in bracket
low_pos, high_pos = (0, 1) if bracket_f[0] <= bracket_f[-1] else (1, 0)
while not done and ls_iter < max_ls:
# line-search bracket is so small
if paddle.abs(bracket[1] - bracket[0]) * d_norm < tolerance_change:
break
# compute new trial value
alpha = _cubic_interpolate(
bracket[0],
bracket_f[0],
bracket_gtd[0],
bracket[1],
bracket_f[1],
bracket_gtd[1],
)
# test that we are making sufficient progress:
# in case `alpha` is so close to boundary, we mark that we are making
# insufficient progress, and if
# + we have made insufficient progress in the last step, or
# + `alpha` is at one of the boundary,
# we will move `alpha` to a position which is `0.1 * len(bracket)`
# away from the nearest boundary point.
eps = 0.1 * (max(bracket) - min(bracket))
if min(max(bracket) - alpha, alpha - min(bracket)) < eps:
# interpolation close to boundary
if insuf_progress or alpha >= max(bracket) or alpha <= min(bracket):
# evaluate at 0.1 away from boundary
if paddle.abs(alpha - max(bracket)) < paddle.abs(
alpha - min(bracket)
):
alpha = max(bracket) - eps
else:
alpha = min(bracket) + eps
insuf_progress = False
else:
insuf_progress = True
else:
insuf_progress = False
# Evaluate new point
loss_new, grad_new = obj_func(xk, alpha, d)
ls_func_evals += 1
gtd_new = grad_new.dot(d)
ls_iter += 1
if (
loss_new > (loss + c1 * alpha * gtd)
or loss_new >= bracket_f[low_pos]
):
# Armijo condition not satisfied or not lower than lowest point
bracket[high_pos] = alpha
bracket_f[high_pos] = loss_new
# bracket_g[high_pos] = grad_new.clone(memory_format=torch.contiguous_format)
bracket_g[high_pos] = grad_new.clone()
bracket_gtd[high_pos] = gtd_new
low_pos, high_pos = (
(0, 1) if bracket_f[0] <= bracket_f[1] else (1, 0)
)
else:
if paddle.abs(gtd_new) <= -c2 * gtd:
# Wolfe conditions satisfied
done = True
elif gtd_new * (bracket[high_pos] - bracket[low_pos]) >= 0:
# old high becomes new low
bracket[high_pos] = bracket[low_pos]
bracket_f[high_pos] = bracket_f[low_pos]
bracket_g[high_pos] = bracket_g[low_pos]
bracket_gtd[high_pos] = bracket_gtd[low_pos]
# new point becomes new low
bracket[low_pos] = alpha
bracket_f[low_pos] = loss_new
bracket_g[low_pos] = grad_new.clone()
bracket_gtd[low_pos] = gtd_new
# return stuff
alpha = bracket[low_pos]
loss_new = bracket_f[low_pos]
grad_new = bracket_g[low_pos]
return loss_new, grad_new, alpha, ls_func_evals
@@ -0,0 +1,361 @@
# Copyright (c) 2020 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 __future__ import annotations
from typing import TYPE_CHECKING
import paddle
from paddle.base import framework, unique_name
from paddle.base.dygraph import base as imperative_base
from paddle.base.framework import Variable
from paddle.base.layer_helper import LayerHelper
from paddle.framework import in_pir_mode
from paddle.optimizer import Optimizer
from paddle.pir.core import create_parameter
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.framework import Operator
from paddle.static import Program
__all__ = []
class LookAhead(Optimizer):
r"""
This implements the Lookahead optimizer of the
paper : https://arxiv.org/abs/1907.08610.
Lookahead keeps two sets of params: the fast_params and
the slow_params. inner_optimizer update fast_params every
training step. Lookahead updates the slow_params and fast_params
every k training steps as follows:
.. math::
slow\_param_t &= slow\_param_{t-1} + \\alpha * (fast\_param_{t-1} - slow\_param_{t-1})
fast\_param_t &= slow\_param_t
Args:
inner_optimizer (Optimizer): The optimizer that update fast params step by step.
alpha (float, optional): The learning rate of Lookahead. The default value is 0.5.
k (int, optional): The slow params is updated every k steps. The default value is 5.
name (str, optional): Normally there is no need for user to set this property.
For more information, please refer to :ref:`api_guide_Name`.
The default value is None.
Examples:
.. code-block:: pycon
>>> import numpy as np
>>> import paddle
>>> import paddle.nn as nn
>>> BATCH_SIZE = 16
>>> BATCH_NUM = 4
>>> EPOCH_NUM = 4
>>> IMAGE_SIZE = 784
>>> CLASS_NUM = 10
>>> # define a random dataset
>>> class RandomDataset(paddle.io.Dataset): # type: ignore[type-arg]
... def __init__(self, num_samples):
... self.num_samples = num_samples
...
... def __getitem__(self, idx):
... image = np.random.random([IMAGE_SIZE]).astype('float32')
... label = np.random.randint(0, CLASS_NUM - 1, (1,)).astype('int64')
... return image, label
...
... def __len__(self):
... return self.num_samples
>>> class LinearNet(nn.Layer):
... def __init__(self):
... super().__init__()
... self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)
... self.bias = self._linear.bias
...
... @paddle.jit.to_static
... def forward(self, x):
... return self._linear(x)
>>> def train(layer, loader, loss_fn, opt):
... for epoch_id in range(EPOCH_NUM):
... for batch_id, (image, label) in enumerate(loader()):
... out = layer(image)
... loss = loss_fn(out, label)
... loss.backward()
... opt.step()
... opt.clear_grad()
... print("Train Epoch {} batch {}: loss = {}".format(epoch_id, batch_id, np.mean(loss.numpy())))
>>> layer = LinearNet()
>>> loss_fn = nn.CrossEntropyLoss()
>>> optimizer = paddle.optimizer.SGD(learning_rate=0.1, parameters=layer.parameters())
>>> lookahead = paddle.incubate.LookAhead(optimizer, alpha=0.2, k=5)
>>> # create data loader
>>> dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
>>> loader = paddle.io.DataLoader(
... dataset,
... batch_size=BATCH_SIZE,
... shuffle=True,
... drop_last=True,
... num_workers=2,
... )
>>> # doctest: +SKIP('The run time is too long to pass the CI check.')
>>> train(layer, loader, loss_fn, lookahead)
"""
inner_optimizer: Optimizer
alpha: float
k: int
type: str
helper: LayerHelper
_slow_str = "slow"
def __init__(
self,
inner_optimizer: Optimizer,
alpha: float = 0.5,
k: int = 5,
name: str | None = None,
) -> None:
assert inner_optimizer is not None, "inner optimizer can not be None"
assert 0.0 <= alpha <= 1.0, (
"alpha should be larger or equal to 0.0, and less or equal than 1.0"
)
assert isinstance(k, int) and k > 0, "k should be a positive integer"
self.inner_optimizer = inner_optimizer
if self.inner_optimizer._parameter_list is None:
parameters = (
paddle.static.default_main_program()
.global_block()
.all_parameters()
)
else:
parameters = self.inner_optimizer._parameter_list
super().__init__(
learning_rate=alpha,
parameters=parameters,
weight_decay=None,
grad_clip=None,
name=name,
)
self.alpha = alpha
self.k = k
self.type = "lookahead"
self.helper = LayerHelper(self.__class__.__name__)
self._global_step_var = None
self._k_var = None
def _set_auxiliary_var(self, key, val):
super()._set_auxiliary_var(key, val)
self.inner_optimizer._set_auxiliary_var(key, val)
@framework.dygraph_only
@imperative_base.no_grad
def step(self) -> None:
"""
Execute the optimizer and update parameters once.
Returns:
None
Examples:
.. code-block:: pycon
>>> import paddle
>>> inp = paddle.rand([1, 10], dtype="float32")
>>> linear = paddle.nn.Linear(10, 1)
>>> out = linear(inp)
>>> loss = paddle.mean(out)
>>> sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters())
>>> lookahead = paddle.incubate.LookAhead(sgd, alpha=0.2, k=5)
>>> loss.backward()
>>> lookahead.step()
>>> lookahead.clear_grad()
"""
self.inner_optimizer.step()
self._increment_global_var()
params_grads = []
for param in self._parameter_list:
if not param.trainable:
continue
if param._grad_ivar() is not None:
grad_var = param._grad_ivar()
params_grads.append((param, grad_var))
self._apply_optimize(
loss=None, startup_program=None, params_grads=params_grads
)
def _create_accumulators(self, block, parameters):
assert isinstance(block, (framework.Block, paddle.pir.Block))
for p in parameters:
self._add_accumulator(self._slow_str, p)
def _increment_global_var(self):
if in_pir_mode():
if self._global_step_var is None:
self._global_step_var = create_parameter(
dtype='int32',
shape=[1],
name=unique_name.generate("lookahead_step"),
trainable=False,
initializer=paddle.nn.initializer.ConstantInitializer(
value=0.0, force_cpu=False
),
)
self._global_step_var = paddle.increment(self._global_step_var, 1.0)
else:
if self._global_step_var is None:
self._global_step_var = paddle.static.create_global_var(
name=unique_name.generate("lookahead_step"),
shape=[1],
value=0,
dtype='int32',
persistable=True,
)
self.helper.append_op(
type='increment',
inputs={'X': [self._global_step_var]},
outputs={'Out': [self._global_step_var]},
attrs={'step': 1.0},
)
def _append_optimize_op(self, block, param_and_grad):
one_var = paddle.ones(shape=[1], dtype='int32', name='lookahead_ones')
zero_var = paddle.zeros(
shape=[1], dtype='int32', name='lookahead_zeros'
)
if in_pir_mode():
k_var = create_parameter(
dtype='int32',
shape=[1],
name=unique_name.generate("lookahead_k"),
trainable=False,
initializer=paddle.nn.initializer.ConstantInitializer(
value=float(self.k), force_cpu=False
),
)
else:
k_var = paddle.static.create_global_var(
name=unique_name.generate("lookahead_k"),
shape=[1],
value=self.k,
dtype='int32',
persistable=True,
)
mod = paddle.remainder(self._global_step_var, k_var)
cond_1 = paddle.equal(self._global_step_var, one_var)
cond_1 = paddle.cast(cond_1, dtype='float32')
cond_2 = paddle.equal(mod, zero_var)
cond_2 = paddle.cast(cond_2, dtype='float32')
slow_var = self._get_accumulator(self._slow_str, param_and_grad[0])
tmp_var = cond_1 * param_and_grad[0] + (1 - cond_1) * slow_var
paddle.assign(tmp_var, slow_var)
tmp_var = self.alpha * param_and_grad[0] + (1.0 - self.alpha) * slow_var
tmp_var_1 = cond_2 * tmp_var + (1 - cond_2) * param_and_grad[0]
paddle.assign(tmp_var_1, param_and_grad[0])
tmp_var_1 = cond_2 * tmp_var + (1 - cond_2) * slow_var
paddle.assign(tmp_var_1, slow_var)
@imperative_base.no_grad
def minimize(
self,
loss: Tensor,
startup_program: Program | None = None,
parameters: list[Tensor] | list[str] | None = None,
no_grad_set: set[Tensor] | set[str] | None = None,
) -> tuple[list[Operator], list[tuple[Tensor, Tensor]]]:
"""
Add operations to minimize ``loss`` by updating ``parameters``.
Args:
loss (Tensor): A ``Tensor`` containing the value to minimize.
startup_program (Program, optional): :ref:`api_paddle_static_Program` for
initializing parameters in ``parameters``. The default value
is None, at this time :ref:`api_paddle_static_default_startup_program` will be used.
parameters (list, optional): List of ``Tensor`` or ``Tensor.name`` to update
to minimize ``loss``. The default value is None, at this time all parameters
will be updated.
no_grad_set (set, optional): Set of ``Tensor`` or ``Tensor.name`` that don't need
to be updated. The default value is None.
Returns:
tuple: tuple (optimize_ops, params_grads), A list of operators appended
by minimize and a list of (param, grad) tensor pairs, param is
``Parameter``, grad is the gradient value corresponding to the parameter.
In static graph mode, the returned tuple can be passed to ``fetch_list`` in ``Executor.run()`` to
indicate program pruning. If so, the program will be pruned by ``feed`` and
``fetch_list`` before run, see details in ``Executor``.
Examples:
.. code-block:: pycon
>>> import paddle
>>> inp = paddle.rand([1, 10], dtype="float32")
>>> linear = paddle.nn.Linear(10, 1)
>>> out = linear(inp)
>>> loss = paddle.mean(out)
>>> sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters())
>>> lookahead = paddle.incubate.LookAhead(sgd, alpha=0.2, k=5)
>>> loss.backward()
>>> lookahead.minimize(loss)
>>> lookahead.clear_grad()
"""
assert isinstance(loss, (Variable, paddle.pir.Value)), (
"The loss should be an Tensor."
)
# Apply inner optimizer to the main_program
optimize_ops, params_grads = self.inner_optimizer.minimize(
loss,
startup_program=startup_program,
parameters=parameters,
no_grad_set=no_grad_set,
)
self._increment_global_var()
_ = self._apply_optimize(
loss, startup_program=startup_program, params_grads=params_grads
)
return optimize_ops, params_grads
@@ -0,0 +1,625 @@
# Copyright (c) 2020 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 __future__ import annotations
from typing import TYPE_CHECKING
import paddle
from paddle import _C_ops
from paddle.base import framework
from paddle.base.dygraph import base as imperative_base
from paddle.base.layer_helper import LayerHelper
from paddle.base.wrapped_decorator import signature_safe_contextmanager
from paddle.framework import (
in_dynamic_mode,
in_dynamic_or_pir_mode,
in_pir_mode,
)
from paddle.optimizer import Optimizer
if TYPE_CHECKING:
from collections.abc import Generator, Sequence
from paddle import Tensor
from paddle.optimizer.optimizer import _ParameterConfig
from paddle.static import Executor, Program
__all__ = []
class ModelAverage(Optimizer):
r"""
The ModelAverage optimizer accumulates specific continuous historical
parameters during training. The accumulated historical range can be controlled
by the passed ``average_window_rate`` argument. The averaged ``Parameter`` are
used in the prediction, which usually can improve the accuracy of the prediction.
Accumulate the average of the ``Parameter`` in the sliding window, the result will be saved
in a temporary variable, can be applied to the current model's ``Parameter`` by calling
the ``apply()`` method, and the current model ``Parameter`` can be restored by calling
the ``restore()`` method.
The window size for calculating the average is determined by ``average_window_rate``,
``min_average_window``, ``max_average_window`` and the current ``Parameter`` update times (num_updates).
When the cumulative times (num_accumulates) is greater than the specific window
threshold (average_window), the accumulated ``Parameter`` temporary variable is set to 0.0.
The following example will help to understand the role of these arguments:
::
if num_accumulates >= min_average_window and num_accumulates >= min(max_average_window, num_updates * average_window_rate):
num_accumulates = 0
In the above conditional judgment statement, ``num_accumulates`` indicates the current
accumulated number, which can be abstractly understood as the length of the cumulative window.
The length of the window must be at least the length set by the ``min_average_window`` argument,
and cannot exceed the length specified by the ``max_average_window`` argument or
``num_updates * average_window_rate``, where ``num_updates`` indicates the current ``Parameter``
update times, ``average_window_rate`` is a coefficient that calculates the length of the window.
Args:
average_window_rate (float): The calculate ratio of the window length relative to ``Parameter`` update times.
parameters (list, optional): List of ``Tensor`` names to update to minimize ``loss``. \
This parameter is required in dygraph mode. \
The default value is None in static graph mode, at this time all parameters will be updated.
min_average_window (int, optional): the minimum size of average window length. The default value is 10000.
max_average_window (int, optional): The maximum size of average window length. The default value is 10000.
name (str, optional): Normally there is no need for user to set this property.
For more information, please refer to :ref:`api_guide_Name`.
The default value is None.
Examples:
.. code-block:: pycon
>>> # doctest: +SKIP("Cannot get source code by to_static in REPL")
>>> import numpy as np
>>> import paddle
>>> import paddle.nn as nn
>>> import paddle.optimizer as opt
>>> BATCH_SIZE = 16
>>> BATCH_NUM = 4
>>> EPOCH_NUM = 4
>>> IMAGE_SIZE = 784
>>> CLASS_NUM = 10
>>> # define a random dataset
>>> class RandomDataset(paddle.io.Dataset): # type: ignore[type-arg]
... def __init__(self, num_samples):
... self.num_samples = num_samples
... def __getitem__(self, idx):
... image = np.random.random([IMAGE_SIZE]).astype('float32')
... label = np.random.randint(0, CLASS_NUM - 1, (1, )).astype('int64')
... return image, label
... def __len__(self):
... return self.num_samples
...
>>> class LinearNet(nn.Layer):
... def __init__(self):
... super().__init__()
... self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)
... self.bias = self._linear.bias
...
... @paddle.jit.to_static
... def forward(self, x):
... return self._linear(x)
...
>>> def train(layer, loader, loss_fn, opt, model_average):
... for epoch_id in range(EPOCH_NUM):
... for batch_id, (image, label) in enumerate(loader()):
... out = layer(image)
... loss = loss_fn(out, label)
... loss.backward()
... opt.step()
... model_average.step()
... opt.clear_grad()
... model_average.clear_grad()
... print("Train Epoch {} batch {}: loss = {}, bias = {}".format(
... epoch_id, batch_id, np.mean(loss.numpy()), layer.bias.numpy()))
...
>>> def evaluate(layer, loader, loss_fn):
... for batch_id, (image, label) in enumerate(loader()):
... out = layer(image)
... loss = loss_fn(out, label)
... loss.backward()
... print("Evaluate batch {}: loss = {}, bias = {}".format(
... batch_id, np.mean(loss.numpy()), layer.bias.numpy()))
...
>>> # create network
>>> layer = LinearNet()
>>> loss_fn = nn.CrossEntropyLoss()
>>> optimizer = opt.Momentum(learning_rate=0.2, momentum=0.1, parameters=layer.parameters())
>>> model_average = paddle.incubate.ModelAverage(
... 0.15,
... parameters=layer.parameters(),
... min_average_window=2,
... max_average_window=10
... )
...
>>> # create data loader
>>> dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
>>> loader = paddle.io.DataLoader(dataset,
... batch_size=BATCH_SIZE,
... shuffle=True,
... drop_last=True,
... num_workers=2)
...
>>> # create data loader
>>> eval_loader = paddle.io.DataLoader(dataset,
... batch_size=BATCH_SIZE,
... shuffle=True,
... drop_last=True,
... num_workers=1
... )
...
>>> # train
>>> train(layer, loader, loss_fn, optimizer, model_average)
>>> print("\nEvaluate With ModelAverage")
>>> with model_average.apply(need_restore=False):
... evaluate(layer, eval_loader, loss_fn)
>>> print("\nEvaluate With Restored Parameters")
>>> model_average.restore()
>>> evaluate(layer, eval_loader, loss_fn)
"""
helper: LayerHelper
average_window: float
min_average_window: int
max_average_window: int
type: str
apply_program: Program
restore_program: Program
def __init__(
self,
average_window_rate: float,
parameters: Sequence[Tensor] | Sequence[_ParameterConfig] | None = None,
min_average_window: int = 10000,
max_average_window: int = 10000,
name: str | None = None,
) -> None:
super().__init__(
learning_rate=0.0,
parameters=parameters,
weight_decay=None,
grad_clip=None,
name=name,
)
self.helper = LayerHelper(self.__class__.__name__)
self.average_window = average_window_rate
self.min_average_window = min_average_window
self.max_average_window = max_average_window
self.type = "average_accumulates"
if not in_dynamic_mode():
global_block = paddle.static.default_main_program().global_block()
all_parameters = (
parameters if parameters else global_block.all_parameters()
)
self._create_accumulators(global_block, all_parameters)
for param in all_parameters:
self._append_optimize_op(global_block, [param, None])
self.apply_program = paddle.static.Program()
block = self.apply_program.global_block()
with paddle.static.program_guard(main_program=self.apply_program):
for param in all_parameters:
self._add_average_apply_op(block, param)
self.restore_program = paddle.static.Program()
block = self.restore_program.global_block()
with paddle.static.program_guard(main_program=self.restore_program):
for param in all_parameters:
self._add_average_restore_op(block, param)
def _create_accumulators(self, block, parameters):
assert isinstance(block, (framework.Block, paddle.pir.Block))
for param in parameters:
self._add_accumulator('sum_1', param)
self._add_accumulator('sum_2', param)
self._add_accumulator('sum_3', param)
self._add_accumulator('restore', param)
self._add_accumulator(
'num_accumulates', param, dtype='int64', shape=[1]
)
self._add_accumulator(
'old_num_accumulates', param, dtype='int64', shape=[1]
)
self._add_accumulator(
'num_updates', param, dtype='int64', shape=[1]
)
def _append_optimize_op(self, block, param_and_grad):
assert isinstance(block, (framework.Block, paddle.pir.Block))
sum_1 = self._get_accumulator('sum_1', param_and_grad[0])
sum_2 = self._get_accumulator('sum_2', param_and_grad[0])
sum_3 = self._get_accumulator('sum_3', param_and_grad[0])
num_accumulates = self._get_accumulator(
'num_accumulates', param_and_grad[0]
)
old_num_accumulates = self._get_accumulator(
'old_num_accumulates', param_and_grad[0]
)
num_updates = self._get_accumulator('num_updates', param_and_grad[0])
if in_dynamic_or_pir_mode():
_, _, _, _, _, _ = _C_ops.average_accumulates_(
param_and_grad[0],
sum_1,
sum_2,
sum_3,
num_accumulates,
old_num_accumulates,
num_updates,
self.average_window,
self.max_average_window,
self.min_average_window,
)
return None
block = framework.default_main_program().global_block()
attrs = {
"average_window": self.average_window,
"min_average_window": self.min_average_window,
"max_average_window": self.max_average_window,
}
inputs = {
"param": param_and_grad[0],
"in_sum_1": sum_1,
"in_sum_2": sum_2,
"in_sum_3": sum_3,
"in_num_accumulates": num_accumulates,
"in_old_num_accumulates": old_num_accumulates,
"in_num_updates": num_updates,
}
outputs = {
"out_sum_1": sum_1,
"out_sum_2": sum_2,
"out_sum_3": sum_3,
"out_num_accumulates": num_accumulates,
"out_old_num_accumulates": old_num_accumulates,
"out_num_updates": num_updates,
}
average_accumulates_op = block.append_op(
type=self.type,
inputs=inputs,
outputs=outputs,
attrs=attrs,
stop_gradient=True,
)
return average_accumulates_op
@imperative_base.no_grad
def minimize(
self,
loss: Tensor,
startup_program: Program | None = None,
parameters: list[Tensor] | None = None,
no_grad_set: set[Tensor] | set[str] | None = None,
) -> None:
"""
Add operations to minimize ``loss`` by updating ``parameters``.
Args:
loss (Tensor): A ``Tensor`` containing the value to minimize.
startup_program (Program, optional): :ref:`api_paddle_static_Program` for
initializing parameters in ``parameters``. The default value
is None, at this time :ref:`api_paddle_static_default_startup_program` will be used.
parameters (list, optional): List of ``Tensor`` or ``Tensor.name`` to update
to minimize ``loss``. The default value is None, at this time all parameters
will be updated.
no_grad_set (set, optional): Set of ``Tensor`` or ``Tensor.name`` that don't need
to be updated. The default value is None.
Returns:
tuple: tuple (optimize_ops, params_grads), A list of operators appended
by minimize and a list of (param, grad) tensor pairs, param is
``Parameter``, grad is the gradient value corresponding to the parameter.
In static graph mode, the returned tuple can be passed to ``fetch_list`` in ``Executor.run()`` to
indicate program pruning. If so, the program will be pruned by ``feed`` and
``fetch_list`` before run, see details in ``Executor``.
Examples:
.. code-block:: pycon
>>> import paddle
>>> inp = paddle.rand([1, 10], dtype="float32")
>>> linear = paddle.nn.Linear(10, 1)
>>> out = linear(inp)
>>> loss = paddle.mean(out)
>>> loss.backward()
>>> sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters())
>>> sgd.minimize(loss)
>>> modelaverage = paddle.incubate.ModelAverage(
... 0.15,
... parameters=linear.parameters(),
... min_average_window=2,
... max_average_window=4,
... )
>>> modelaverage.minimize(loss)
>>> sgd.clear_grad()
>>> modelaverage.clear_grad()
"""
if in_dynamic_mode():
self.step()
@framework.dygraph_only
@imperative_base.no_grad
def step(self) -> None:
"""
Execute the optimizer and update parameters once.
Returns:
None
Examples:
.. code-block:: pycon
>>> import paddle
>>> inp = paddle.rand([1, 10], dtype="float32")
>>> linear = paddle.nn.Linear(10, 1)
>>> out = linear(inp)
>>> loss = paddle.mean(out)
>>> sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters())
>>> modelaverage = paddle.incubate.ModelAverage(
... 0.15,
... parameters=linear.parameters(),
... min_average_window=2,
... max_average_window=4,
... )
>>> loss.backward()
>>> sgd.step()
>>> modelaverage.step()
>>> sgd.clear_grad()
>>> modelaverage.clear_grad()
"""
params_grads = []
for param in self._parameter_list:
if not param.trainable:
continue
if param._grad_ivar() is not None:
grad_var = param._grad_ivar()
params_grads.append((param, grad_var))
block = framework.default_main_program().global_block()
self._create_accumulators(block, self._parameter_list)
for param_and_grad in params_grads:
self._append_optimize_op(block, param_and_grad)
@signature_safe_contextmanager
@imperative_base.no_grad
def apply(
self, executor: Executor | None = None, need_restore: bool = True
) -> Generator[None, None, None]:
"""
Apply the average of the cumulative ``Parameter`` to the parameters of the current model.
Args:
executor(Executor): The network executor in static-graph mode. The default value is None in dygraph mode.
need_restore(bool): Restore flag variable, if set to True, the network will restore
the parameters of the network to the default value, if set to False,
it will not be restored. The default value is True.
Examples:
.. code-block:: pycon
>>> import paddle
>>> inp = paddle.rand([1, 10], dtype="float32")
>>> linear = paddle.nn.Linear(10, 1)
>>> out = linear(inp)
>>> loss = paddle.mean(out)
>>> loss.backward()
>>> sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters())
>>> modelaverage = paddle.incubate.ModelAverage(
... 0.15,
... parameters=linear.parameters(),
... min_average_window=2,
... max_average_window=4,
... )
>>> sgd.step()
>>> modelaverage.step()
>>> with modelaverage.apply():
... for param in linear.parameters():
... print(param)
>>> for param in linear.parameters():
... print(param)
"""
if in_dynamic_mode():
for param in self._parameter_list:
num_accumulates = self._get_accumulator(
'num_accumulates', param
)
old_num_accumulates = self._get_accumulator(
'old_num_accumulates', param
)
sum_1 = self._get_accumulator('sum_1', param)
sum_2 = self._get_accumulator('sum_2', param)
sum_3 = self._get_accumulator('sum_3', param)
param_restore = self._get_accumulator('restore', param)
paddle.assign(param, param_restore)
total_param = sum_1 + sum_2 + sum_3
total_accumulates = num_accumulates + old_num_accumulates
total_param = paddle.cast(total_param, dtype='float32')
total_accumulates = paddle.cast(
total_accumulates, dtype='float32'
)
average_param = total_param / total_accumulates
paddle.assign(average_param, param)
try:
yield
finally:
if need_restore:
self.restore()
return
if executor is None:
raise RuntimeError(
"Executor should not be None in static graph mode."
)
executor.run(self.apply_program)
try:
yield
finally:
if need_restore:
self.restore(executor)
@imperative_base.no_grad
def restore(self, executor: Executor | None = None) -> None:
"""
Restore ``Parameter`` values of current model.
Args:
executor(Executor): The network executor in static-graph mode. The default value is None in dygraph mode
Examples:
.. code-block:: pycon
>>> import paddle
>>> inp = paddle.rand([1, 10], dtype="float32")
>>> linear = paddle.nn.Linear(10, 1)
>>> out = linear(inp)
>>> loss = paddle.mean(out)
>>> loss.backward()
>>> sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters())
>>> modelaverage = paddle.incubate.ModelAverage(
... 0.15,
... parameters=linear.parameters(),
... min_average_window=2,
... max_average_window=4,
... )
>>> sgd.step()
>>> modelaverage.step()
>>> with modelaverage.apply(need_restore=False):
... for param in linear.parameters():
... print(param)
>>> for param in linear.parameters():
... print(param)
>>> modelaverage.restore()
>>> for param in linear.parameters():
... print(param)
"""
if in_dynamic_mode():
for param in self._parameter_list:
param_restore = self._get_accumulator('restore', param)
paddle.assign(param_restore, param)
return
if executor is None:
raise RuntimeError(
"Executor should not be None in static graph mode."
)
executor.run(self.restore_program)
def _add_average_apply_op(self, block, param):
if in_pir_mode():
target_program = paddle.static.default_main_program()
param = paddle.pir.core._get_parameter(target_program, param)
restore_value = self._get_accumulator('restore', param)
grad = paddle.pir.core._get_persistable_value(
target_program, restore_value
)
sum_1 = self._get_accumulator('sum_1', param)
sum_1 = paddle.pir.core._get_persistable_value(
target_program, sum_1
)
sum_2 = self._get_accumulator('sum_2', param)
sum_2 = paddle.pir.core._get_persistable_value(
target_program, sum_2
)
sum_3 = self._get_accumulator('sum_3', param)
sum_3 = paddle.pir.core._get_persistable_value(
target_program, sum_3
)
num_accumulates = self._get_accumulator('num_accumulates', param)
num_accumulates = paddle.pir.core._get_persistable_value(
target_program, num_accumulates
)
old_num_accumulates = self._get_accumulator(
'old_num_accumulates', param
)
old_num_accumulates = paddle.pir.core._get_persistable_value(
target_program, old_num_accumulates
)
else:
param = block._clone_variable(param)
grad = block._clone_variable(
self._get_accumulator('restore', param)
)
sum_1 = block._clone_variable(self._get_accumulator('sum_1', param))
sum_2 = block._clone_variable(self._get_accumulator('sum_2', param))
sum_3 = block._clone_variable(self._get_accumulator('sum_3', param))
num_accumulates = block._clone_variable(
self._get_accumulator('num_accumulates', param)
)
old_num_accumulates = block._clone_variable(
self._get_accumulator('old_num_accumulates', param)
)
# backup param value to grad
paddle.assign(param, output=grad)
# param = (sum_1 + sum_2 + sum_3) / (num_accumulates + old_num_accumulates)
tmp = paddle.add_n([num_accumulates, old_num_accumulates])
sum = paddle.add_n([sum_1, sum_2, sum_3])
tmp = paddle.cast(
x=tmp, dtype='float32' if self._dtype is None else self._dtype
)
sum = paddle.cast(
x=sum, dtype='float32' if self._dtype is None else self._dtype
)
divide_out = paddle.divide(x=sum, y=tmp)
paddle.assign(divide_out, output=param)
def _add_average_restore_op(self, block, param):
if in_pir_mode():
target_program = paddle.static.default_main_program()
param = paddle.pir.core._get_parameter(target_program, param)
restore_value = self._get_accumulator('restore', param)
grad = paddle.pir.core._get_persistable_value(
target_program, restore_value
)
else:
param = block._clone_variable(param)
grad = block._clone_variable(
self._get_accumulator('restore', param)
)
paddle.assign(grad, output=param)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,811 @@
# Copyright (c) 2019 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 logging
import paddle
from paddle.base import core, framework, unique_name
from paddle.base.backward import append_backward
from paddle.base.framework import Variable, in_dygraph_mode, program_guard
from paddle.optimizer import Optimizer
class RecomputeOptimizer(Optimizer):
"""
:api_attr: Static Graph
Recompute Optimizer Wrapper
Normally, a training step contains three sub-steps: first, run forward
Operators to calculate the loss; second, run backward Operators to
calculate gradient of the parameters; third, apply optimization method
to update the value of the parameters.
In the forward computation process, all variables that are needed by
backward computation process will be kept in memory, which occupy a great
amount of memory when the network becomes very deep.
Recompute split the network to k segments. In each segment, It will
recompute the forward Operators, before running backward operators. It is
very helpful for saving memory.
The Variables that separate a network to segments are called as checkpoints,
and users should set it manually. The usage is very simple:
Args:
optimizer (Optimizer): The optimizer that is applied to parameters.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import numpy as np
>>> paddle.enable_static()
>>> def gen_data():
... return {
... "x": np.random.random(size=(32, 32)).astype('float32'),
... "y": np.random.randint(2, size=(32, 1)).astype('int64'),
... }
>>> def mlp(input_x, input_y, hid_dim=128, label_dim=2):
... print(input_x)
... fc_1 = paddle.static.nn.fc(x=input_x, size=hid_dim)
... prediction = paddle.static.nn.fc(x=[fc_1], size=label_dim, activation='softmax')
... cost = paddle.nn.functional.cross_entropy(
... input=prediction,
... label=input_y,
... reduction='none',
... use_softmax=False,
... )
... sum_cost = paddle.mean(cost)
... return sum_cost, fc_1, prediction
>>> input_x = paddle.static.data(name="x", shape=[-1, 32], dtype='float32')
>>> input_y = paddle.static.data(name="y", shape=[-1, 1], dtype='int64')
>>> cost, fc_1, pred = mlp(input_x, input_y)
>>> sgd = paddle.optimizer.Adam(learning_rate=0.01)
>>> sgd = paddle.incubate.optimizer.RecomputeOptimizer(sgd)
>>> sgd._set_checkpoints([fc_1, pred])
>>> sgd.minimize(cost)
>>> print("Finished optimize")
Finished optimize
>>> place = paddle.CPUPlace()
>>> exe = paddle.static.Executor(place)
>>> exe.run(paddle.static.default_startup_program())
>>> step = 10
>>> for i in range(step):
... cost_val = exe.run(
... feed=gen_data(),
... program=paddle.static.default_main_program(),
... fetch_list=[cost.name],
... )
... print("step=%d cost=%f" % (i, cost_val[0]))
var x : DENSE_TENSOR.shape(-1, 32).dtype(float32).stop_gradient(True)
Finished optimize
step=0 cost=0.737203
step=1 cost=1.308077
step=2 cost=0.768422
step=3 cost=1.239475
step=4 cost=0.882643
step=5 cost=0.738027
step=6 cost=0.819374
step=7 cost=0.818534
step=8 cost=0.753692
step=9 cost=0.787448
"""
def __init__(self, optimizer):
if in_dygraph_mode():
raise Exception("In dygraph, don't support RecomputeOptimizer.")
self._optimizer = optimizer
self._checkpoints = None
self._learning_rate = self._optimizer._learning_rate
self._learning_rate_map = self._optimizer._learning_rate_map
self.enable_offload = False
def _set_checkpoints(self, checkpoints):
"""
Args:
checkpoints (list): List of Variable or string
"""
assert isinstance(checkpoints, list), (
"_checkpoints should be a list of Variable or a list of String"
)
for ckpt in checkpoints:
assert isinstance(ckpt, (Variable, str)), (
"_checkpoints should be a list of Variable or a list of String"
)
self._checkpoints = checkpoints
# should enable offload before calling backward
def _enable_offload(self):
self.enable_offload = True
@framework.deprecate_stat_dict
def load(self, state_dict):
"""
:api_attr: Static Graph
load function is not supported by Recompute Optimizer for now.
:return: None
Args:
state_dict: the dict load by load_persistable method
Examples:
.. code-block:: pycon
>>> import paddle
>>> paddle.enable_static()
>>> def mlp(input_x, input_y, hid_dim=128, label_dim=2):
... fc_1 = paddle.static.nn.fc(x=input_x, size=hid_dim)
... prediction = paddle.static.nn.fc(x=[fc_1], size=label_dim, activation='softmax')
... cost = paddle.nn.functional.cross_entropy(
... input=prediction,
... label=input_y,
... reduction='none',
... use_softmax=False,
... )
... sum_cost = paddle.mean(cost)
... return sum_cost, fc_1, prediction
>>> input_x = paddle.static.data(name="x", shape=[-1, 32], dtype='float32')
>>> input_y = paddle.static.data(name="y", shape=[-1, 1], dtype='int64')
>>> cost, fc_1, pred = mlp(input_x, input_y)
>>> print("Finished FF")
Finished FF
>>> sgd = paddle.optimizer.Adam(learning_rate=0.01)
>>> sgd = paddle.incubate.optimizer.RecomputeOptimizer(sgd)
>>> sgd._set_checkpoints([fc_1, pred])
>>> try:
... state_dict = {}
... sgd.load(state_dict)
>>> except NotImplementedError as e:
... print(e)
load function is not supported by Recompute Optimizer for now
"""
raise NotImplementedError(
"load function is not supported by Recompute Optimizer for now"
)
def apply_gradients(self, params_grads):
"""
call apply_gradients function of self._optimizer.
Args:
params_grads (list): list of (param, grad) pair to do optimization.
Returns:
list: A list of operators appended to the current program.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.base.framework as framework
>>> paddle.enable_static()
>>> def mlp(input_x, input_y, hid_dim=128, label_dim=2):
... fc_1 = paddle.static.nn.fc(x=input_x, size=hid_dim)
... prediction = paddle.static.nn.fc(x=[fc_1], size=label_dim, activation='softmax')
... cost = paddle.nn.functional.cross_entropy(
... input=prediction,
... label=input_y,
... reduction='none',
... use_softmax=False,
... )
... sum_cost = paddle.mean(cost)
... return sum_cost, fc_1, prediction
>>> input_x = paddle.static.data(name="x", shape=[-1, 32], dtype='float32')
>>> input_y = paddle.static.data(name="y", shape=[-1, 1], dtype='int64')
>>> cost, fc_1, pred = mlp(input_x, input_y)
>>> print("Finished FF")
Finished FF
>>> sgd = paddle.optimizer.Adam(learning_rate=0.01)
>>> sgd = paddle.incubate.optimizer.RecomputeOptimizer(sgd)
>>> sgd._set_checkpoints([fc_1, pred])
>>> params_grads = sgd.backward(
... cost,
... startup_program=None,
... parameter_list=None,
... no_grad_set=None,
... )
>>> program = cost.block.program
>>> with framework.program_guard(program, None):
... optimize_ops = sgd.apply_gradients(params_grads)
>>> print("Finished apply gradients")
Finished apply gradients
"""
return self._optimizer.apply_gradients(params_grads=params_grads)
def _create_vars(self, varname):
pinned_var_name = unique_name.generate(varname + "@Pinned")
fetched_var_name = unique_name.generate(varname + "@Fetch")
pinned_var = self._main_program.global_block().create_var(
name=pinned_var_name,
shape=self.checkpoint_shape,
dtype=self._main_program.global_block().var(varname).dtype,
persistable=False,
stop_gradient=True,
)
fetch_var = self._main_program.global_block().create_var(
name=fetched_var_name,
shape=self.checkpoint_shape,
dtype=self._main_program.global_block().var(varname).dtype,
persistable=False,
stop_gradient=False,
)
return pinned_var_name, fetched_var_name
def _append_fill_constant_ops(self, startup_program):
"""
add fill_constant_ops to the end of the prog
we should fill the pinned vars before running the main_prog
to instantiate their tensor hold_, which could tell us whether
the host memory could hold all the checkpoints from all the
GPU devices in this node.
"""
op_role = 0
block = startup_program.global_block()
fill_constant_vars = self.checkpoint_name2pinned_name.values()
OP_ROLE_KEY = core.op_proto_and_checker_maker.kOpRoleAttrName()
for varname in fill_constant_vars:
var = self._main_program.global_block().var(varname)
# NOTE (JZ-LIANG) to pre-allocate the CUDAPinned MEM
pinned_var = block.create_var(
name=varname,
shape=self.checkpoint_shape,
dtype=self._main_program.global_block().var(var.name).dtype,
persistable=False,
stop_gradient=True,
)
block.append_op(
type='fill_constant',
outputs={'Out': varname},
attrs={
"shape": var.shape,
"dtype": var.dtype,
"value": 0.0,
"place_type": 2,
OP_ROLE_KEY: op_role,
},
)
def _insert_async_memcpy_op(
self, insert_idx, src_varname, dst_varname, op_role, dst_place_type
):
OP_ROLE_KEY = core.op_proto_and_checker_maker.kOpRoleAttrName()
self.block._insert_op_without_sync(
insert_idx,
type='memcpy',
inputs={'X': [self._main_program.global_block().var(src_varname)]},
outputs={
'Out': [self._main_program.global_block().var(dst_varname)]
},
attrs={"dst_place_type": int(dst_place_type), OP_ROLE_KEY: op_role},
)
def _insert_fetch_op(self, idx, varname):
assert varname in self.checkpoint_name2pinned_name, (
f"Try to fetch {varname} from Pinned Memory, but it is NOT a checkpoint"
)
pinned_varname = self.checkpoint_name2pinned_name[varname]
fetch_varname = self.checkpoint_name2fetch_name[varname]
self._insert_async_memcpy_op(idx, pinned_varname, fetch_varname, 1, 1)
def _insert_offload_op(self, idx, varname):
assert varname in self.checkpoint_name2pinned_name, (
f"Try to offload {varname} to Pinned Memory, but it is NOT a checkpoint"
)
pinned_varname = self.checkpoint_name2pinned_name[varname]
self._insert_async_memcpy_op(idx, varname, pinned_varname, 0, 2)
def _insert_sync_op(self, op_idx, checkpoint_name):
# single stream offload no need sync
pass
def _record_fetch_op(self, idx):
assert len(self.un_fetch_checkpoint_names) > 0, (
"Could NOT found checkpoint to fetch"
)
checkpoint_name = self.un_fetch_checkpoint_names.pop(-1)
logging.debug(f"Record fetch [{checkpoint_name}]")
self.idx2insertions[idx] = ("fetch", checkpoint_name)
return checkpoint_name
def _record_offload_op(self, idx, checkpoint_name):
expected_checkpoint_name = self.un_offload_checkpoint_names.pop(0)
assert checkpoint_name == expected_checkpoint_name, (
f"expected to offload [{expected_checkpoint_name}] but got [{checkpoint_name}]"
)
logging.debug(f"Record offload [{checkpoint_name}]")
self.idx2insertions[idx] = ("offload", checkpoint_name)
def _record_sync_op(self, idx, checkpoint_name):
assert checkpoint_name not in self.synced_checkpoints, (
f"Try to sync the checkpoint [{checkpoint_name}] twice"
)
self.synced_checkpoints.add(checkpoint_name)
logging.debug(f"Record offload sync [{checkpoint_name}]")
self.idx2insertions[idx] = ("sync", checkpoint_name)
def _parse_backward(self):
self.idx2insertions = {}
# don't offload the last checkpoints, to favor throughput
self.un_fetch_checkpoint_names = self.sorted_checkpoint_names[:]
self.un_fetch_checkpoint_names.pop(-1)
need_fetch_checkpoint_names = self.un_fetch_checkpoint_names[:]
self.checkpoint_usage_count = {}
for checkpoint_name in self.un_fetch_checkpoint_names:
self.checkpoint_usage_count[checkpoint_name] = 0
self.bw_start_op_idx = len(self.block.ops)
for idx, op in enumerate(self.block.ops):
if int(op.desc.attr("op_role")) == 1:
self.bw_start_op_idx = idx
break
assert self.bw_start_op_idx < len(self.block.ops), (
"Could NOT found backward op in prog"
)
# fetch second to last checkpoint at the beginning of BW
fetched_checkpoint_varname = self._record_fetch_op(self.bw_start_op_idx)
last_last_fetch_checkpoint = None
for i, op in enumerate(self.block.ops[self.bw_start_op_idx :]):
idx = self.bw_start_op_idx + i
input_vars = op.desc.input_arg_names()
for input_var in input_vars:
if input_var in need_fetch_checkpoint_names:
if input_var not in self.un_fetch_checkpoint_names:
# fetch the offload checkpoint when the first usage of its previous one
if self.checkpoint_usage_count[input_var] == 0:
# TODO (JZ-LIANG) sync memcpy_stream if extra stream for memcpy
second_to_last_fetch_checkpoint = (
fetched_checkpoint_varname
)
# there is NO fetch ahead the first checkpoint
if input_var != self.sorted_checkpoint_names[0]:
fetched_checkpoint_varname = (
self._record_fetch_op(idx)
)
# should check the current used checkpoint is the last fetch one
assert second_to_last_fetch_checkpoint == input_var, (
f"Current recompute segment should use [{second_to_last_fetch_checkpoint}] BUT got [{input_var}]"
)
# rename
self.block.ops[idx]._rename_input(
input_var,
self.checkpoint_name2fetch_name[input_var],
)
self.checkpoint_usage_count[input_var] += 1
else:
raise ValueError(
f"use checkpoint [{input_var}] before fetch in BW"
)
assert len(self.un_fetch_checkpoint_names) == 0, (
f"{self.un_fetch_checkpoint_names} checkpoints have NOT been Recorded"
)
def _update_backward(self):
if len(self.idx2insertions) == 0:
return
total_op = len(self.block.ops)
for op_idx in reversed(range(self.bw_start_op_idx, total_op)):
if op_idx in self.idx2insertions:
operation, checkpoint_name = self.idx2insertions[op_idx]
if operation == "fetch":
self._insert_fetch_op(op_idx, checkpoint_name)
logging.debug(f"Insert [{checkpoint_name}] fetch op.")
del self.idx2insertions[op_idx]
elif operation == "sync":
self._insert_sync_op(op_idx, checkpoint_name)
logging.debug(f"Sync [{checkpoint_name}] fetch op.")
self.block._sync_with_cpp()
assert len(self.idx2insertions) == 0, (
f"{[ele[1] for ele in self.idx2insertions.values()]} checkpoints left un-Fetched"
)
def _parse_forward(self):
self.idx2insertions = {}
# don't offload the last checkpoints, faster, less memory saving
self.un_offload_checkpoint_names = self.sorted_checkpoint_names[:]
last_checkpoint = self.un_offload_checkpoint_names.pop(-1)
need_offload_checkpoint_names = self.un_offload_checkpoint_names[:]
self.checkpoint_usage_count_and_idx = {}
for checkpoint_name in self.un_offload_checkpoint_names:
self.checkpoint_usage_count_and_idx[checkpoint_name] = {
'count': 0,
'idx': -1,
}
self.synced_checkpoints = set()
self.fw_start_op_idx = len(self.block.ops)
for idx, op in enumerate(self.block.ops):
if int(op.desc.attr("op_role")) == 0:
self.fw_start_op_idx = idx
break
assert self.fw_start_op_idx < len(self.block.ops), (
"Could NOT found Forward op in prog"
)
last_offload_checkpoint = None
for i, op in enumerate(
self.block.ops[self.fw_start_op_idx : self.bw_start_op_idx]
):
idx = self.fw_start_op_idx + i
output_vars = op.desc.output_arg_names()
input_vars = op.desc.input_arg_names()
for output_var in output_vars:
if output_var in need_offload_checkpoint_names:
assert len(output_vars) == 1, (
f"checkpoint should be the only Output of a certain op, but [{output_var}] is from [{op}]"
)
if output_var in self.un_offload_checkpoint_names:
# insert sync op if last checkpoint has not been sync
if last_offload_checkpoint is not None:
if (
self.checkpoint_usage_count_and_idx[
last_offload_checkpoint
]['count']
== 0
):
self._record_sync_op(
idx, last_offload_checkpoint
)
else:
last_usage_idx = (
self.checkpoint_usage_count_and_idx[
last_offload_checkpoint
]['idx']
)
assert last_usage_idx > 0, (
f"last_usage_idx of checkpoint [{last_offload_checkpoint}] should large than 0"
)
self._record_sync_op(
last_usage_idx + 1, last_offload_checkpoint
)
# insert offload op after the checkpoint's generation op
self._record_offload_op(idx + 1, output_var)
last_offload_checkpoint = output_var
else:
raise ValueError(
f"There should be just ONE op that output checkpoint [{output_var}]"
)
# need to sync the last need to offload checkpoint before the last checkpoint as output op
if output_var == last_checkpoint:
assert len(output_vars) == 1, (
f"checkpoint should be the only Output of a certain op, but [{output_var}] is from [{op}]"
)
assert (
last_offload_checkpoint
== self.sorted_checkpoint_names[-2]
), (
f"the last offload checkpoint before [{last_checkpoint}] is suppose to be [{self.sorted_checkpoint_names[-2]}], but got [{last_offload_checkpoint}]"
)
# sync if last checkpoint has not been sync
if (
self.checkpoint_usage_count_and_idx[
last_offload_checkpoint
]['idx']
== 0
):
self._record_sync_op(idx, last_offload_checkpoint)
else:
last_usage_idx = self.checkpoint_usage_count_and_idx[
last_offload_checkpoint
]['idx']
assert last_usage_idx > 0, (
f"last_usage_idx of checkpoint [{last_offload_checkpoint}] should large than 0"
)
self._record_sync_op(
last_usage_idx + 1, last_offload_checkpoint
)
# record checkpoint usage
for input_var in input_vars:
if input_var in need_offload_checkpoint_names:
assert input_var not in self.synced_checkpoints, (
f"checkpoint [{input_var}] used after sync"
)
self.checkpoint_usage_count_and_idx[input_var]['count'] += 1
self.checkpoint_usage_count_and_idx[input_var]['idx'] = idx
assert len(self.un_offload_checkpoint_names) == 0, (
f"{self.un_fetch_checkpoint_names} checkpoints have NOT been Recorded"
)
assert len(self.synced_checkpoints) == len(
need_offload_checkpoint_names
), (
f"{set(need_offload_checkpoint_names) - set(self.synced_checkpoints)} checkpoints have NOT been Recorded"
)
def _update_forward(self):
if len(self.idx2insertions) == 0:
return
for op_idx in reversed(
range(self.fw_start_op_idx, self.bw_start_op_idx)
):
if op_idx in self.idx2insertions:
operation, checkpoint_name = self.idx2insertions[op_idx]
if operation == "offload":
self._insert_offload_op(op_idx, checkpoint_name)
logging.debug(f"Insert [{checkpoint_name}] offload op.")
del self.idx2insertions[op_idx]
elif operation == "sync":
self._insert_sync_op(op_idx, checkpoint_name)
logging.debug(
f"Insert [{checkpoint_name}] offload_sync op."
)
del self.idx2insertions[op_idx]
self.block._sync_with_cpp()
assert len(self.idx2insertions) == 0, (
f"{[ele[1] for ele in self.idx2insertions.values()]} checkpoints left un-Offloaded"
)
def _check_offload_fetch(self):
# TODO(JZ-LIANG) the single stream offload need no sync
pass
def _offload(self, loss, startup_program=None):
"""
core steps for recompute offload
1. create pinned vars and temp vars
2. parse & update Forward pass: offload, sync
3. parse & update Backward pass: rename, fetch, sync
4. verify the correctness
"""
self._main_program = loss.block.program
self.block = loss.block
if startup_program is None:
startup_program = paddle.static.default_startup_program()
with program_guard(self._main_program, startup_program):
assert len(self.checkpoint_shape) > 0, (
f"checkpoints shape {self.checkpoint_shape} should be an non empty list like: [12, 512, 1024]"
)
assert all(ele > 0 for ele in self.checkpoint_shape), (
f"all ele in checkpoints shape {self.checkpoint_shape} should be a determined integer larger than 0"
)
self.checkpoint_name2pinned_name = {}
self.checkpoint_name2fetch_name = {}
for checkpoint_varname in self.sorted_checkpoint_names:
pinned_var_name, fetch_var_name = self._create_vars(
checkpoint_varname
)
self.checkpoint_name2pinned_name[checkpoint_varname] = (
pinned_var_name
)
self.checkpoint_name2fetch_name[checkpoint_varname] = (
fetch_var_name
)
self._append_fill_constant_ops(startup_program)
# TODO (JZ-LIANG) to provide two offload strategy in future
# step 2. parse & update FW: rename, offload, sync
self._parse_backward()
self._update_backward()
# step 3. parse & update BW: rename, offload, sync
self._parse_forward()
self._update_forward()
# step 4. verify the correctness
self._check_offload_fetch()
def backward(
self,
loss,
startup_program=None,
parameter_list=None,
no_grad_set=None,
callbacks=None,
):
"""
call append_backward with checkpoints.
Args:
loss (Variable): loss variable to run optimizations.
startup_program (Program): startup_program for initializing parameters
in `parameter_list`.
parameter_list (list): list of Variables or Variable.names to update.
no_grad_set (set|None): set of Variables or Variables.names should be ignored.
callbacks (list|None): list of callables to run when appending backward
operator for one parameter.
checkpoints (list): list of Variables as checkpoints
Examples:
.. code-block:: pycon
>>> import paddle
>>> paddle.enable_static()
>>> def mlp(input_x, input_y, hid_dim=128, label_dim=2):
... fc_1 = paddle.static.nn.fc(x=input_x, size=hid_dim)
... prediction = paddle.static.nn.fc(x=[fc_1], size=label_dim, activation='softmax')
... cost = paddle.nn.functional.cross_entropy(
... input=prediction,
... label=input_y,
... reduction='none',
... use_softmax=False,
... )
... sum_cost = paddle.mean(cost)
... return sum_cost, fc_1, prediction
>>> input_x = paddle.static.data(name="x", shape=[-1, 32], dtype='float32')
>>> input_y = paddle.static.data(name="y", shape=[-1, 1], dtype='int64')
>>> cost, fc_1, pred = mlp(input_x, input_y)
>>> print("Finished FF")
Finished FF
>>> sgd = paddle.optimizer.Adam(learning_rate=0.01)
>>> sgd = paddle.incubate.optimizer.RecomputeOptimizer(sgd)
>>> sgd._set_checkpoints([fc_1, pred])
>>> params_grads = sgd.backward(
... cost,
... startup_program=None,
... parameter_list=None,
... no_grad_set=None,
... )
>>> print("Finished backward")
Finished backward
"""
assert self._checkpoints is not None, (
"You should call _set_checkpoints first"
)
if in_dygraph_mode():
raise NotImplementedError(
"DyGraph current does not support recompute"
)
self._dtype = loss.dtype
program = loss.block.program
with program_guard(program, startup_program):
checkpoint_vars = []
for ckpt in self._checkpoints:
if isinstance(ckpt, Variable):
checkpoint_vars.append(ckpt)
else:
checkpoint_vars.append(loss.block.var(ckpt))
# allow return to non-recompute when checkpoints is empty
if len(checkpoint_vars) > 0:
params_grads, sorted_checkpoint_names = append_backward(
loss,
parameter_list,
no_grad_set,
checkpoints=checkpoint_vars,
)
else:
params_grads = append_backward(
loss,
parameter_list,
no_grad_set,
checkpoints=checkpoint_vars,
)
if self.enable_offload:
self.sorted_checkpoint_names = sorted_checkpoint_names
self._offload(loss, startup_program=startup_program)
return params_grads
def apply_optimize(self, loss, startup_program, params_grads):
"""
call the apply_optimize function of self._optimizer
Args:
loss (Variable): loss variable to run optimizations.
startup_program (Program): startup_program for initializing parameters
in `parameter_list`.
params_grads (list): list of (param, grad) pair to do optimization.
Examples:
.. code-block:: pycon
>>> import paddle
>>> paddle.enable_static()
>>> def mlp(input_x, input_y, hid_dim=128, label_dim=2):
... fc_1 = paddle.static.nn.fc(x=input_x, size=hid_dim)
... prediction = paddle.static.nn.fc(x=[fc_1], size=label_dim, activation='softmax')
... cost = paddle.nn.functional.cross_entropy(
... input=prediction,
... label=input_y,
... reduction='none',
... use_softmax=False,
... )
... sum_cost = paddle.mean(cost)
... return sum_cost, fc_1, prediction
>>> input_x = paddle.static.data(name="x", shape=[-1, 32], dtype='float32')
>>> input_y = paddle.static.data(name="y", shape=[-1, 1], dtype='int64')
>>> cost, fc_1, pred = mlp(input_x, input_y)
>>> print("Finished FF")
Finished FF
>>> sgd = paddle.optimizer.Adam(learning_rate=0.01)
>>> sgd = paddle.incubate.optimizer.RecomputeOptimizer(sgd)
>>> sgd._set_checkpoints([fc_1, pred])
>>> params_grads = sgd.backward(
... cost,
... startup_program=None,
... parameter_list=None,
... no_grad_set=None,
... )
>>> optimize_ops = sgd.apply_optimize(
... cost,
... startup_program=None,
... params_grads=params_grads,
... )
>>> print("Finished apply_optimize")
Finished apply_optimize
"""
func = (
self._optimizer.apply_optimize
if hasattr(self._optimizer, 'apply_optimize')
else self._optimizer._apply_optimize
)
return func(
loss, startup_program=startup_program, params_grads=params_grads
)
def minimize(
self, loss, startup_program=None, parameter_list=None, no_grad_set=None
):
assert isinstance(loss, Variable), "The loss should be an Variable."
assert self._checkpoints is not None, (
"You should call _set_checkpoints first"
)
if in_dygraph_mode():
raise NotImplementedError(
"DyGraph current does not support recompute"
)
params_grads = self.backward(
loss,
startup_program=startup_program,
parameter_list=parameter_list,
no_grad_set=no_grad_set,
)
optimize_ops = self.apply_optimize(
loss, startup_program=startup_program, params_grads=params_grads
)
return optimize_ops, params_grads