/* 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* 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 void Run(const std::vector& input_values, const std::vector& expected_grad_values) { Run(ClientSession(scope_), input_values, expected_grad_values); } template void Run(const ClientSession& session, const std::vector& input_values, const std::vector& 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 run_outputs; std::vector 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( out_tensors[i], test::AsTensor({expected_grad_values[i]}, {})); } } Scope scope_; std::vector inputs_; std::vector outputs_; std::vector grad_outputs_; }; TEST_F(WhileGradientsTest, Basic) { // Create loop: while (i < 10) i += 1 Init(1); CreateLoop( [](const Scope& s, const std::vector& inputs, Output* output) { *output = ops::Less(s, inputs[0], 10); return s.status(); }, [](const Scope& s, const std::vector& inputs, std::vector* 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({1}, {1}); Run({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& inputs, Output* output) { *output = ops::Less(s, inputs[0], 10); return s.status(); }, [](const Scope& s, const std::vector& inputs, std::vector* 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({0, 1, 2}, {1, 5, 1}); Run({1, 1, 0}, {1, 5, 1}); Run({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 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& 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& inputs, std::vector* 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({1.0, 1.0}, {-2.0, 2.0}); Run({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& inputs, Output* output) { *output = ops::Less(s, inputs[0], 10); return s.status(); }, [](const Scope& s, const std::vector& inputs, std::vector* 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(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