Repository: chainer/chainer

Issue:
`F.cast` unexpected gradient when dtypes are the same
Revision: Current master 8e0d84ef53322029524ce24ca457fe1f51ff9e70
Code:
```py
import chainer
from chainer import functions as F
import numpy

src_dtype = numpy.float32
dst_dtype = numpy.float64
x = chainer.Variable(numpy.array([7], src_dtype))

y1 = F.cast(x, dst_dtype)
y2 = F.cast(x, dst_dtype)
z = y1 + y2

gy1, gy2 = chainer.grad([z], [y1, y2], [numpy.ones_like(z.array)])
print(gy1)
print(gy2)
```

Result for `src_dtype != dst_dtype`: as expected
```
variable([1.])
variable([1.])
```

Result for `src_dtype == dst_dtype`: not as expected
```
variable([2.])
variable([2.])
```



Discussion:

Resolving patch:
diff --git a/chainer/functions/array/cast.py b/chainer/functions/array/cast.py
--- a/chainer/functions/array/cast.py
+++ b/chainer/functions/array/cast.py
@@ -52,5 +52,6 @@ def cast(x, typ):
 
     """
     if x.dtype == typ:
-        return chainer.as_variable(x)
+        if not chainer.config.enable_backprop:
+            return chainer.as_variable(x)
     return Cast(typ).apply((x,))[0]


Test patch:
diff --git a/tests/chainer_tests/functions_tests/array_tests/test_cast.py b/tests/chainer_tests/functions_tests/array_tests/test_cast.py
--- a/tests/chainer_tests/functions_tests/array_tests/test_cast.py
+++ b/tests/chainer_tests/functions_tests/array_tests/test_cast.py
@@ -35,8 +35,8 @@ def setUp(self):
     def check_forward(self, x_data):
         x = chainer.Variable(x_data)
         y = functions.cast(x, self.out_type)
-        self.assertEqual(y.data.shape, x.data.shape)
-        self.assertEqual(y.data.dtype, self.out_type)
+        assert y.data.shape == x.data.shape
+        assert y.data.dtype == self.out_type
 
     def test_forward_cpu(self):
         self.check_forward(self.x)
@@ -65,18 +65,34 @@ def setUp(self):
 
     def check_forward_no_cast(self, x_data):
         y = functions.cast(x_data, self.dtype)
-        self.assertIsInstance(y, chainer.Variable)
-        self.assertIs(y.data, x_data)
+        assert isinstance(y, chainer.Variable)
+        assert y.data is x_data
 
     def test_forward_no_cast_array(self):
         y = functions.cast(self.x, self.dtype)
-        self.assertIsInstance(y, chainer.Variable)
-        self.assertIs(y.data, self.x)
+        assert isinstance(y, chainer.Variable)
+        assert y.data is self.x
 
     def test_forward_no_cast_variable(self):
+        # If backprop is disabled, it's safe to simply return the input
+        # variable for no-op casts.
         x = chainer.Variable(self.x)
-        y = functions.cast(x, self.dtype)
-        self.assertIs(y, x)
+        with chainer.using_config('enable_backprop', False):
+            y = functions.cast(x, self.dtype)
+        assert y is x
+
+    def test_forward_no_cast_grad(self):
+        # This test would fail if F.cast does not create new function nodes for
+        # no-op casts
+        x = chainer.Variable(self.x)
+        y1 = functions.cast(x, self.dtype)
+        y2 = functions.cast(x, self.dtype)
+        z = y1 + y2
+        gy1, gy2 = chainer.grad([z], [y1, y2], [numpy.ones_like(z.data)])
+        assert gy1.dtype == self.dtype
+        assert gy2.dtype == self.dtype
+        numpy.testing.assert_array_equal(gy1.data, numpy.ones_like(y1.data))
+        numpy.testing.assert_array_equal(gy2.data, numpy.ones_like(y2.data))
 
 
 testing.run_module(__name__, __file__)