# Copyright (c) 2025 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 unittest import numpy as np import paddle from paddle.compat.nn import Linear class TestCompatLinearLayer(unittest.TestCase): def setUp(self): self.seed = 42 np.random.seed(self.seed) paddle.seed(self.seed) def get_error_range(self, is_large=False): # xpu matmul precision is very low, rtol cannot be set if paddle.core.is_compiled_with_xpu(): return (0, 0.1) if is_large: return (1e-1, 1e-4) return (1e-3, 1e-6) def _numpy_linear_forward(self, x, weight, bias=None): """NumPy implementation of linear forward pass""" # Torch linear: y = x @ weight.T + bias # So we need to transpose weight for NumPy implementation result = np.dot(x, weight.T) if bias is not None: result += bias return result def _numpy_linear_backward(self, x, weight, bias, dy): """NumPy implementation of linear backward pass""" x_shape = x.shape dy_shape = dy.shape # Reshape to 2D: (batch_size * other_dims, in_features) x_2d = x.reshape(-1, x_shape[-1]) dy_2d = dy.reshape(-1, dy_shape[-1]) # dx = dy @ weight dx_2d = np.dot(dy_2d, weight) # dw = dy.T @ x dw = np.dot(dy_2d.T, x_2d) # db = sum(dy, axis=all_but_last) db = np.sum(dy_2d, axis=0) if bias is not None else None # Reshape dx back to original input shape (except last dimension) dx = dx_2d.reshape(*x_shape[:-1], dx_2d.shape[-1]) return dx, dw, db def _create_linear_layer( self, in_features, out_features, bias=True, weight_np=None, bias_np=None, dtype=None, ): """Create Linear layer with specific weights""" linear = Linear(in_features, out_features, bias=bias, dtype=dtype) # Set custom weights if provided if weight_np is not None: linear.weight.set_value(paddle.to_tensor(weight_np)) if bias and bias_np is not None: linear.bias.set_value(paddle.to_tensor(bias_np)) return linear def _compare_forward(self, x_np, weight_np, bias_np=None, dtype=None): """Compare forward pass with NumPy implementation""" # NumPy calculation y_np = self._numpy_linear_forward(x_np, weight_np, bias_np) # Paddle calculation with Linear layer in_features = weight_np.shape[1] out_features = weight_np.shape[0] linear = self._create_linear_layer( in_features, out_features, bias=(bias_np is not None), weight_np=weight_np, bias_np=bias_np, dtype=dtype, ) x_pd = paddle.to_tensor(x_np) y_pd = linear(x_pd) # Compare results rtol, atol = self.get_error_range(is_large=x_np.size > 8192) np.testing.assert_allclose(y_pd.numpy(), y_np, rtol=rtol, atol=atol) def _compare_backward(self, x_np, weight_np, bias_np=None, dtype=None): """Compare backward pass with NumPy implementation""" in_features = weight_np.shape[1] out_features = weight_np.shape[0] # Create Linear layer with custom weights linear = self._create_linear_layer( in_features, out_features, bias=(bias_np is not None), weight_np=weight_np, bias_np=bias_np, dtype=dtype, ) # Prepare input tensor x_pd = paddle.to_tensor(x_np, stop_gradient=False) # Forward pass y_pd = linear(x_pd) # Create upstream gradient (same shape as output) dy_np = np.random.randn(*y_pd.shape).astype(x_np.dtype) dy_pd = paddle.to_tensor(dy_np) # Backward pass y_pd.backward(dy_pd) # NumPy gradients dx_np, dw_np, db_np = self._numpy_linear_backward( x_np, weight_np, bias_np, dy_np ) rtol, atol = self.get_error_range(is_large=x_np.size > 8192) # Compare gradients np.testing.assert_allclose( x_pd.grad.numpy(), dx_np, rtol=rtol, atol=atol ) np.testing.assert_allclose( linear.weight.grad.numpy(), dw_np, rtol=rtol, atol=atol ) if bias_np is not None: np.testing.assert_allclose( linear.bias.grad.numpy(), db_np, rtol=rtol, atol=atol ) def test_2d_input_with_bias(self): """Test 2D input with bias""" x_np = np.random.randn(4, 3).astype(np.float32) weight_np = np.random.randn(5, 3).astype(np.float32) bias_np = np.random.randn(5).astype(np.float32) self._compare_forward(x_np, weight_np, bias_np) self._compare_backward(x_np, weight_np, bias_np) def test_2d_input_no_bias(self): """Test 2D input without bias""" x_np = np.random.randn(4, 3).astype(np.float32) weight_np = np.random.randn(5, 3).astype(np.float32) self._compare_forward(x_np, weight_np, None) self._compare_backward(x_np, weight_np, None) def test_1d_input_with_bias(self): """Test 1D input (no batch dimension) with bias""" x_np = np.random.randn(3).astype(np.float32) weight_np = np.random.randn(5, 3).astype(np.float32) bias_np = np.random.randn(5).astype(np.float32) self._compare_forward(x_np, weight_np, bias_np) self._compare_backward(x_np, weight_np, bias_np) def test_out_features_one(self): """Test Linear(10, 1) with 1D and 2D input""" weight_np = np.random.randn(1, 10).astype(np.float32) bias_np = np.random.randn(1).astype(np.float32) linear = paddle.compat.nn.Linear(10, 1) linear.weight.set_value(paddle.to_tensor(weight_np)) linear.bias.set_value(paddle.to_tensor(bias_np)) x_np = np.random.randn(10).astype(np.float32) y_np = self._numpy_linear_forward(x_np, weight_np, bias_np) y_pd = linear(paddle.to_tensor(x_np)) rtol, atol = self.get_error_range() np.testing.assert_allclose(y_pd.numpy(), y_np, rtol=rtol, atol=atol) x_np = np.random.randn(1, 10).astype(np.float32) y_np = self._numpy_linear_forward(x_np, weight_np, bias_np) y_pd = linear(paddle.to_tensor(x_np)) np.testing.assert_allclose(y_pd.numpy(), y_np, rtol=rtol, atol=atol) def test_3d_input_with_bias(self): """Test 3D input with bias""" x_np = np.random.randn(2, 4, 3).astype(np.float32) weight_np = np.random.randn(5, 3).astype(np.float32) bias_np = np.random.randn(5).astype(np.float32) self._compare_forward(x_np, weight_np, bias_np) self._compare_backward(x_np, weight_np, bias_np) def test_4d_input_no_bias(self): """Test 4D input without bias""" x_np = np.random.randn(2, 3, 4, 5).astype(np.float32) weight_np = np.random.randn(6, 5).astype(np.float32) self._compare_forward(x_np, weight_np, None) self._compare_backward(x_np, weight_np, None) def test_large_input_with_bias(self): """Test large input dimensions with bias""" x_np = np.random.randn(128, 512).astype(np.float32) weight_np = np.random.randn(256, 512).astype(np.float32) bias_np = np.random.randn(256).astype(np.float32) self._compare_forward(x_np, weight_np, bias_np) self._compare_backward(x_np, weight_np, bias_np) def test_non_contiguous_shapes(self): """Test non-power-of-two shapes""" x_np = np.random.randn(31, 63).astype(np.float32) weight_np = np.random.randn(127, 63).astype(np.float32) bias_np = np.random.randn(127).astype(np.float32) self._compare_forward(x_np, weight_np, bias_np) self._compare_backward(x_np, weight_np, bias_np) def test_different_dtypes(self): """Test different data types""" dtypes = ["float32", "float64"] if paddle.base.is_compiled_with_cuda(): dtypes.append("float16") for dtype in dtypes: x_np = np.random.randn(4, 3).astype(dtype) weight_np = np.random.randn(5, 3).astype(dtype) bias_np = np.random.randn(5).astype(dtype) self._compare_forward(x_np, weight_np, bias_np, dtype) self._compare_backward(x_np, weight_np, bias_np, dtype) def test_static_graph_simple(self): """Test Linear layer in static graph mode""" if not paddle.base.is_compiled_with_cuda(): return paddle.enable_static() try: program = paddle.static.Program() startup_program = paddle.static.Program() with paddle.static.program_guard(program, startup_program): # Create input data x = paddle.static.data(name='x', shape=[2, 3], dtype='float32') # Create Linear layer (let it initialize its own weights) linear = Linear(3, 4, bias=True) y = linear(x) # Get weight and bias tensors for GT calculation weight = linear.weight bias = linear.bias place = paddle.CUDAPlace(0) exe = paddle.static.Executor(place) exe.run(startup_program) # Simple deterministic input x_np = np.ones([2, 3], dtype=np.float32) # Run and get results including weight and bias results = exe.run( feed={'x': x_np}, fetch_list=[y, weight, bias] ) y_pd, weight_np, bias_np = results # Calculate GT using numpy with the actual weights from Linear layer y_gt = self._numpy_linear_forward(x_np, weight_np, bias_np) # Compare results np.testing.assert_allclose(y_pd, y_gt, rtol=1e-5, atol=1e-8) finally: paddle.disable_static() def test_device_and_dtype_parameters(self): """Test device and dtype parameters""" # Test CPU device linear_cpu = Linear(3, 5, device='cpu', dtype='float32') self.assertEqual(linear_cpu.weight.place.is_cpu_place(), True) self.assertEqual(linear_cpu.weight.dtype, paddle.float32) # if paddle.is_compiled_with_cuda(): # # Test GPU device if available # linear_gpu = Linear(3, 5, device='gpu', dtype='float32') # self.assertEqual(linear_gpu.weight.place.is_gpu_place(), True) # Test different dtype linear_fp64 = Linear(3, 5, dtype='float64') self.assertEqual(linear_fp64.weight.dtype, paddle.float64) def test_weight_initialization(self): """Test weight and bias initialization""" # Test default initialization linear = Linear(10, 20) # Check shape self.assertEqual(linear.weight.shape, [20, 10]) self.assertEqual(linear.bias.shape, [20]) # Check that weights are not all zeros self.assertFalse(np.allclose(linear.weight.numpy(), np.zeros((20, 10)))) # Test without bias linear_no_bias = Linear(10, 20, bias=False) self.assertIsNone(linear_no_bias.bias) def test_edge_cases(self): """Test edge cases""" # Empty input x_np = np.array([]).reshape(0, 3).astype(np.float32) weight_np = np.random.randn(5, 3).astype(np.float32) bias_np = np.random.randn(5).astype(np.float32) self._compare_forward(x_np, weight_np, bias_np) # Single element x_np = np.random.randn(1, 1).astype(np.float32) weight_np = np.random.randn(1, 1).astype(np.float32) bias_np = np.random.randn(1).astype(np.float32) self._compare_forward(x_np, weight_np, bias_np) self._compare_backward(x_np, weight_np, bias_np) def test_weight_transpose_behavior(self): """Test that weight is properly transposed (torch compatibility)""" # Create simple test case where transposition is obvious x_np = np.array([[1.0, 2.0]]).astype(np.float32) # [1, 2] weight_np = np.array([[3.0, 4.0], [5.0, 6.0]]).astype( np.float32 ) # [2, 2] # Manual calculation: x @ weight.T expected = np.array([[1 * 3 + 2 * 4, 1 * 5 + 2 * 6]]).astype( np.float32 ) # [1, 2] # Paddle calculation with Linear layer linear = self._create_linear_layer( 2, 2, weight_np=weight_np, bias=False ) x_pd = paddle.to_tensor(x_np) y_pd = linear(x_pd) np.testing.assert_allclose(y_pd.numpy(), expected, rtol=1e-5, atol=1e-8) def test_reset_parameters(self): if not paddle.base.is_compiled_with_cuda(): return devices = ['cpu', None] # None means the default device for device_ in devices: dummy_tensor = paddle.zeros(1, device=device_) lin = paddle.compat.nn.Linear(4, 8, bias=True, device=device_) expected_device = dummy_tensor.place lin.reset_parameters() self.assertEqual(lin.weight.place, expected_device) self.assertEqual(lin.bias.place, expected_device) def test_error_handling(self): """Test error handling for invalid inputs""" # Shape mismatch between input and weight with self.assertRaises(ValueError): linear = Linear(3, 5) x = paddle.to_tensor( np.random.randn(3, 4).astype(np.float32) ) # Last dim should be 3 linear(x) wrong_api_used = ( "paddle{module}.nn.Linear() received unexpected keyword argument{plural} {args}. " "\nDid you mean to use paddle{correct_module}.nn.Linear() instead?" ) with self.assertRaises(TypeError) as cm: lin = paddle.compat.nn.Linear( 3, 5, weight_attr=None, name='linear_layer', ) self.assertEqual( str(cm.exception), wrong_api_used.format( module=".compat", args="'name', 'weight_attr'", correct_module="", plural="s", ), ) with self.assertRaises(TypeError) as cm: lin = paddle.nn.Linear( 3, 5, bias=True, device="cpu", dtype="float32" ) self.assertEqual( str(cm.exception), wrong_api_used.format( module="", args="'bias', 'device', 'dtype'", correct_module=".compat", plural="s", ), ) def test_state_dict(self): """Test state dict functionality""" linear = Linear(10, 20) # Get state dict state_dict = linear.state_dict() # Check keys self.assertIn('weight', state_dict) self.assertIn('bias', state_dict) # Create new linear and load state new_linear = Linear(10, 20) new_linear.set_state_dict(state_dict) # Check if weights are the same np.testing.assert_allclose( linear.weight.numpy(), new_linear.weight.numpy(), rtol=1e-5, atol=1e-8, ) np.testing.assert_allclose( linear.bias.numpy(), new_linear.bias.numpy(), rtol=1e-5, atol=1e-8 ) def test_parameters_method(self): """Test parameters() method""" linear = Linear(10, 20) # Get parameters params = list(linear.parameters()) # Should return weight and bias self.assertEqual(len(params), 2) self.assertEqual(params[0].shape, [20, 10]) self.assertEqual(params[1].shape, [20]) # Test without bias linear_no_bias = Linear(10, 20, bias=False) params_no_bias = list(linear_no_bias.parameters()) self.assertEqual(len(params_no_bias), 1) # Only weight def test_train_eval_mode(self): """Test train and eval mode""" linear = Linear(10, 20) # Default should be train mode self.assertTrue(linear.training) # Switch to eval mode linear.eval() self.assertFalse(linear.training) # Switch back to train mode linear.train() self.assertTrue(linear.training) if __name__ == "__main__": unittest.main()