Test DPModel in distributed training.
PiperOrigin-RevId: 528039164
This commit is contained in:
parent
e65e14b2d6
commit
8fdac5f833
2 changed files with 188 additions and 0 deletions
|
@ -32,3 +32,12 @@ py_test(
|
|||
"//tensorflow_privacy/privacy/keras_models:dp_keras_model",
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "dp_keras_model_distributed_test",
|
||||
srcs = ["dp_keras_model_distributed_test.py"],
|
||||
deps = [
|
||||
":dp_keras_model",
|
||||
"//tensorflow_privacy/privacy/fast_gradient_clipping:layer_registry",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
# Copyright 2023, The TensorFlow 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.
|
||||
"""Tests DPModel in distributed training."""
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
from absl.testing import parameterized
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
|
||||
from tensorflow_privacy.privacy.fast_gradient_clipping import layer_registry
|
||||
from tensorflow_privacy.privacy.keras_models import dp_keras_model
|
||||
|
||||
|
||||
ds_combinations = tf.__internal__.distribute.combinations
|
||||
|
||||
|
||||
STRATEGIES = [
|
||||
ds_combinations.one_device_strategy,
|
||||
ds_combinations.parameter_server_strategy_1worker_2ps_cpu,
|
||||
]
|
||||
|
||||
|
||||
def get_data(n, clip_norm):
|
||||
# Data is for hidden weights of w* = [3, 1] and bias of b* = 2.
|
||||
# For mean-squared loss, we have:
|
||||
# loss = (data * w + b - label)^2 = (data * (w - w*) + (b - b*))^2
|
||||
# Let T = (data * (w - w*) + (b - b*)), we have:
|
||||
# grad_w = 2 * T * data
|
||||
# grad_b = 2 * T
|
||||
# For w = [0, 0], b = 0:
|
||||
# For data = [3, 4], T = -15, grad_w = [-90, -120], grad_b = -30
|
||||
# For data = [1, -1], T = -4, grad_w = [-8, 8], grad_b = -8
|
||||
data = np.array([[3., 4.], [1., -1.]])
|
||||
labels = np.matmul(data, [[3], [1]]) + 2
|
||||
data, labels = np.tile(data, (n, 1)), np.tile(labels, (n, 1))
|
||||
|
||||
def clip_grad(grad):
|
||||
norm = np.linalg.norm(grad)
|
||||
if norm <= clip_norm:
|
||||
return grad
|
||||
return grad / norm * clip_norm
|
||||
|
||||
grad1 = clip_grad(np.array([-90., -120., -30.]))
|
||||
grad2 = clip_grad(np.array([-8., 8., -8.]))
|
||||
grad = np.mean(np.vstack([grad1, grad2]), axis=0)
|
||||
return data, labels, grad
|
||||
|
||||
|
||||
class DPKerasModelDistributedTest(parameterized.TestCase, tf.test.TestCase):
|
||||
|
||||
@ds_combinations.generate(
|
||||
tf.__internal__.test.combinations.combine(
|
||||
strategy=STRATEGIES,
|
||||
mode="eager",
|
||||
# Clip norm corresponding to no-clipping, clipping one gradient, and
|
||||
# clipping both gradients.
|
||||
clip_norm=[1e5, 200., 1.],
|
||||
model_or_sequential=["model", "sequential"],
|
||||
fast_clipping=[False, True],
|
||||
)
|
||||
)
|
||||
def test_training_works(
|
||||
self, strategy, clip_norm, model_or_sequential, fast_clipping
|
||||
):
|
||||
if model_or_sequential == "sequential" and fast_clipping:
|
||||
self.skipTest("Fast clipping does not work for DPSequential.")
|
||||
|
||||
if isinstance(strategy, tf.distribute.OneDeviceStrategy):
|
||||
strategy_scope = contextlib.nullcontext()
|
||||
else:
|
||||
strategy_scope = strategy.scope()
|
||||
|
||||
n = 10
|
||||
x, y, expected_grad = get_data(n, clip_norm)
|
||||
|
||||
def make_model():
|
||||
dense_layer = tf.keras.layers.Dense(
|
||||
1, kernel_initializer="zeros", bias_initializer="zeros"
|
||||
)
|
||||
dp_kwargs = dict(
|
||||
l2_norm_clip=clip_norm,
|
||||
noise_multiplier=0.0,
|
||||
num_microbatches=None,
|
||||
use_xla=False,
|
||||
layer_registry=layer_registry.make_default_layer_registry()
|
||||
if fast_clipping
|
||||
else None,
|
||||
)
|
||||
if model_or_sequential == "sequential":
|
||||
model = dp_keras_model.DPSequential(
|
||||
layers=[dense_layer],
|
||||
**dp_kwargs,
|
||||
)
|
||||
else:
|
||||
inputs = tf.keras.layers.Input((2,))
|
||||
outputs = dense_layer(inputs)
|
||||
model = dp_keras_model.DPModel(
|
||||
inputs=inputs, outputs=outputs, **dp_kwargs
|
||||
)
|
||||
return model
|
||||
|
||||
with strategy_scope:
|
||||
model = make_model()
|
||||
self.assertEqual(model._enable_fast_peg_computation, fast_clipping)
|
||||
lr = 0.01
|
||||
opt = tf.keras.optimizers.SGD(learning_rate=lr)
|
||||
loss = tf.keras.losses.MeanSquaredError(
|
||||
reduction=tf.keras.losses.Reduction.SUM_OVER_BATCH_SIZE
|
||||
)
|
||||
model.compile(loss=loss, optimizer=opt)
|
||||
history = model.fit(
|
||||
x=x,
|
||||
y=y,
|
||||
epochs=1,
|
||||
batch_size=x.shape[0],
|
||||
steps_per_epoch=1,
|
||||
)
|
||||
self.assertIn("loss", history.history)
|
||||
self.assertEqual(opt.iterations.numpy(), 1)
|
||||
model_weights = model.get_weights()
|
||||
|
||||
expected_val = -expected_grad * lr
|
||||
self.assertAllClose(model_weights[0], expected_val[:2].reshape(-1, 1))
|
||||
self.assertAllClose(model_weights[1], [expected_val[2]])
|
||||
|
||||
|
||||
def _set_spawn_exe_path():
|
||||
"""Set the path to the executable for spawned processes.
|
||||
|
||||
This utility searches for the binary the parent process is using, and sets
|
||||
the executable of multiprocessing's context accordingly.
|
||||
It is branched from tensorflow/python/distribute/multi_process_lib.py, the
|
||||
only change being that it additionally looks under "org_tensorflow_privacy".
|
||||
"""
|
||||
if sys.argv[0].endswith(".py"):
|
||||
|
||||
def guess_path(package_root):
|
||||
# If all we have is a python module path, we'll need to make a guess for
|
||||
# the actual executable path.
|
||||
if "bazel-out" in sys.argv[0] and package_root in sys.argv[0]:
|
||||
# Guess the binary path under bazel. For target
|
||||
# //tensorflow/python/distribute:input_lib_test_multiworker_gpu, the
|
||||
# argv[0] is in the form of
|
||||
# /.../tensorflow/python/distribute/input_lib_test.py
|
||||
# and the binary is
|
||||
# /.../tensorflow/python/distribute/input_lib_test_multiworker_gpu
|
||||
package_root_base = sys.argv[0][: sys.argv[0].rfind(package_root)]
|
||||
binary = os.environ["TEST_TARGET"][2:].replace(":", "/", 1)
|
||||
possible_path = os.path.join(package_root_base, package_root, binary)
|
||||
if os.access(possible_path, os.X_OK):
|
||||
return possible_path
|
||||
return None
|
||||
|
||||
path = (
|
||||
guess_path("org_tensorflow")
|
||||
or guess_path("org_keras")
|
||||
or guess_path("org_tensorflow_privacy")
|
||||
)
|
||||
if path is not None:
|
||||
sys.argv[0] = path
|
||||
multiprocessing.get_context().set_executable(sys.argv[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_set_spawn_exe_path()
|
||||
tf.__internal__.distribute.multi_process_runner.test_main()
|
Loading…
Reference in a new issue