/* Copyright 2017 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.
|
==============================================================================*/
|
|
#include "tensorflow/cc/client/client_session.h"
|
#include "tensorflow/cc/framework/gradients.h"
|
#include "tensorflow/cc/framework/testutil.h"
|
#include "tensorflow/cc/ops/standard_ops.h"
|
#include "tensorflow/cc/ops/while_loop.h"
|
#include "tensorflow/core/framework/graph.pb.h"
|
#include "tensorflow/core/framework/tensor_testutil.h"
|
#include "tensorflow/core/lib/core/status_test_util.h"
|
#include "tensorflow/core/platform/test.h"
|
|
namespace tensorflow {
|
|
namespace {
|
|
class WhileGradientsTest : public ::testing::Test {
|
protected:
|
WhileGradientsTest() : scope_(Scope::NewRootScope()) {}
|
|
void Init(int num_inputs, DataType dtype = DT_INT32) {
|
for (int i = 0; i < num_inputs; ++i) {
|
inputs_.push_back(ops::Placeholder(scope_, dtype));
|
}
|
}
|
|
void CreateLoop(const ops::CondGraphBuilderFn& cond,
|
const ops::BodyGraphBuilderFn& body,
|
const std::vector<Output>* inputs = nullptr) {
|
if (inputs == nullptr) inputs = &inputs_;
|
TF_ASSERT_OK(ops::BuildWhileLoop(scope_, *inputs, cond, body, "test_loop",
|
&outputs_));
|
}
|
|
void CreateBackprop() {
|
TF_ASSERT_OK(
|
AddSymbolicGradients(scope_, outputs_, inputs_, &grad_outputs_));
|
ASSERT_EQ(grad_outputs_.size(), inputs_.size());
|
}
|
|
template <typename T>
|
void Run(const std::vector<Input::Initializer>& input_values,
|
const std::vector<T>& expected_grad_values) {
|
Run<T>(ClientSession(scope_), input_values, expected_grad_values);
|
}
|
|
template <typename T>
|
void Run(const ClientSession& session,
|
const std::vector<Input::Initializer>& input_values,
|
const std::vector<T>& expected_grad_values,
|
const RunOptions& run_options = RunOptions(),
|
RunMetadata* run_metadata = nullptr) {
|
DCHECK_EQ(input_values.size(), inputs_.size());
|
ClientSession::FeedType feeds;
|
for (int i = 0; i < inputs_.size(); ++i) {
|
feeds.emplace(inputs_[i], input_values[i]);
|
}
|
|
std::vector<Operation> run_outputs;
|
std::vector<Tensor> out_tensors;
|
TF_ASSERT_OK(session.Run(run_options, feeds, grad_outputs_, run_outputs,
|
&out_tensors, run_metadata));
|
ASSERT_EQ(out_tensors.size(), grad_outputs_.size());
|
|
DCHECK_EQ(expected_grad_values.size(), out_tensors.size());
|
for (int i = 0; i < out_tensors.size(); ++i) {
|
test::ExpectTensorEqual<T>(
|
out_tensors[i], test::AsTensor<T>({expected_grad_values[i]}, {}));
|
}
|
}
|
|
Scope scope_;
|
std::vector<Output> inputs_;
|
std::vector<Output> outputs_;
|
std::vector<Output> grad_outputs_;
|
};
|
|
TEST_F(WhileGradientsTest, Basic) {
|
// Create loop: while (i < 10) i += 1
|
Init(1);
|
CreateLoop(
|
[](const Scope& s, const std::vector<Output>& inputs, Output* output) {
|
*output = ops::Less(s, inputs[0], 10);
|
return s.status();
|
},
|
[](const Scope& s, const std::vector<Output>& inputs,
|
std::vector<Output>* outputs) {
|
// Use AddN, rather than Add, because the gradient function doesn't
|
// depend on the input shapes, and thus we do not need to store
|
// intermediate values in a stack.
|
outputs->push_back(ops::AddN(s, {inputs[0], 1}));
|
return s.status();
|
});
|
CreateBackprop();
|
|
Run<int>({1}, {1});
|
Run<int>({11}, {1});
|
}
|
|
TEST_F(WhileGradientsTest, MultipleLoopVars) {
|
// Create loop: while (i < 10) i += j; j += 1; k = k
|
Init(3);
|
CreateLoop(
|
[](const Scope& s, const std::vector<Output>& inputs, Output* output) {
|
*output = ops::Less(s, inputs[0], 10);
|
return s.status();
|
},
|
[](const Scope& s, const std::vector<Output>& inputs,
|
std::vector<Output>* outputs) {
|
outputs->push_back(ops::AddN(s, {inputs[0], inputs[1]}));
|
outputs->push_back(ops::AddN(s, {inputs[1], 1}));
|
outputs->push_back(inputs[2]);
|
return s.status();
|
});
|
CreateBackprop();
|
|
// The following execution traces illustrate why we expect dF/dj to be 5:
|
//
|
// i j k
|
// ---------
|
// 0 1 2 <-- initial values
|
// 1 2 2
|
// 3 3 2
|
// 6 4 2
|
// 10 5 2 <-- while output values
|
// outputs sum = 17
|
//
|
// i j k
|
// ---------
|
// 0 2 2 <-- initial values (add 1 to j)
|
// 2 3 2
|
// 5 4 2
|
// 9 5 2
|
// 14 6 2 <-- while output values
|
// outputs sum = 22
|
//
|
// Calculate the "slope" between j=1 and j=2:
|
// 22 - 17 = 5 => dF/dj = 5
|
Run<int>({0, 1, 2}, {1, 5, 1});
|
|
Run<int>({1, 1, 0}, {1, 5, 1});
|
Run<int>({0, 0, 0}, {1, 6, 1});
|
}
|
|
TEST_F(WhileGradientsTest, Chaining) {
|
Init(2, DT_DOUBLE);
|
|
// Multiply each input by 2 before passing to while loop to make sure chaining
|
// works properly
|
std::vector<Output> loop_inputs = {ops::Multiply(scope_, inputs_[0], 2.0),
|
ops::Multiply(scope_, inputs_[1], 2.0)};
|
|
// Create loop: while (i > 0 && j > 0) i -= 1
|
CreateLoop(
|
[](const Scope& s, const std::vector<Output>& inputs, Output* output) {
|
*output = ops::LogicalAnd(s, ops::Greater(s, inputs[0], 0.0),
|
ops::Greater(s, inputs[1], 0.0));
|
return s.status();
|
},
|
[](const Scope& s, const std::vector<Output>& inputs,
|
std::vector<Output>* outputs) {
|
outputs->push_back(ops::AddN(s, {inputs[0], -1.0}));
|
outputs->push_back(inputs[1]);
|
return s.status();
|
},
|
&loop_inputs);
|
|
// Take negative of first output to make sure chaining works properly
|
outputs_[0] = ops::Neg(scope_, outputs_[0]);
|
|
CreateBackprop();
|
|
Run<double>({1.0, 1.0}, {-2.0, 2.0});
|
Run<double>({0.0, 0.0}, {-2.0, 2.0});
|
}
|
|
TEST_F(WhileGradientsTest, MultipleDevices) {
|
// Make sure loop is created on cpu0
|
scope_ = scope_.WithDevice("/cpu:0");
|
|
// Create loop: while (i < 10) i += j
|
Init(2);
|
CreateLoop(
|
[](const Scope& s, const std::vector<Output>& inputs, Output* output) {
|
*output = ops::Less(s, inputs[0], 10);
|
return s.status();
|
},
|
[](const Scope& s, const std::vector<Output>& inputs,
|
std::vector<Output>* outputs) {
|
// Place body on cpu1
|
Scope cpu1_scope = s.WithDevice("/cpu:1");
|
outputs->push_back(ops::AddN(cpu1_scope, {inputs[0], inputs[1]}));
|
outputs->push_back(inputs[1]);
|
return cpu1_scope.status();
|
});
|
|
// Build gradient graph on cpu1
|
Scope cpu1_scope = scope_.WithDevice("/cpu:1");
|
TF_ASSERT_OK(
|
AddSymbolicGradients(cpu1_scope, outputs_, inputs_, &grad_outputs_));
|
ASSERT_EQ(grad_outputs_.size(), inputs_.size());
|
|
// Run with two CPU devices and output partition graphs
|
SessionOptions session_options;
|
(*session_options.config.mutable_device_count())["CPU"] = 2;
|
RunOptions run_options;
|
run_options.set_output_partition_graphs(true);
|
RunMetadata run_metadata;
|
Run<int>(ClientSession(scope_, session_options), {0, 1}, {1, 11}, run_options,
|
&run_metadata);
|
|
// Check that at least one node ran on each device
|
ASSERT_EQ(run_metadata.partition_graphs().size(), 2);
|
for (const GraphDef& partition_graph : run_metadata.partition_graphs()) {
|
EXPECT_GE(partition_graph.node().size(), 1);
|
}
|
}
|
|
} // namespace
|
} // namespace tensorflow
|