Files
2026-07-13 12:40:42 +08:00

521 lines
13 KiB
Python

# 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 random
import numpy as np
import paddle
import paddle.distributed as dist
from paddle import nn
dist.init_parallel_env()
class SimpleConvNet(nn.Layer):
def __init__(
self,
in_channel,
out_channel,
kernel_size,
padding,
bias_attr,
stride,
data_format="NCHW",
):
super().__init__()
self.conv1 = nn.Conv2D(
in_channel,
out_channel,
kernel_size=kernel_size,
padding=padding,
data_format=data_format,
bias_attr=bias_attr,
stride=stride,
)
self.relu = nn.ReLU()
def forward(self, x):
x = self.conv1(x)
return self.relu(x)
class TestTPConv:
def __init__(self):
self.rank = dist.get_rank()
self.world_size = dist.get_world_size()
self._tp_mesh = dist.ProcessMesh(
list(range(self.world_size)), dim_names=["tp"]
)
def set_seed(self, seed):
paddle.seed(seed)
np.random.seed(seed)
random.seed(seed)
def _test_intermediate(
self,
N,
C,
H,
W,
kernel_size,
padding,
bias_attr,
mesh,
test_name="conv_test",
dtype_str="float32",
data_format="NCHW",
stride=1,
):
self.set_seed(2025)
dist.auto_parallel.set_mesh(mesh)
conv_layer = SimpleConvNet(
C,
C,
kernel_size=kernel_size,
padding=padding,
data_format=data_format,
bias_attr=bias_attr,
stride=stride,
)
original_weight = conv_layer.conv1.weight
conv_layer.conv1.weight = original_weight
if data_format == "NCHW":
input_tensor = paddle.randn([N, C, H, W])
shard_axis_input = 3
else:
input_tensor = paddle.randn([N, H, W, C])
shard_axis_input = 2
input_placements = [
dist.Replicate() for _ in range(len(mesh.dim_names))
]
mp_dim_index = mesh.dim_names.index("mp")
input_placements[mp_dim_index] = dist.Shard(shard_axis_input)
sharded_input = dist.shard_tensor(input_tensor, mesh, input_placements)
output_ref = conv_layer(input_tensor)
loss_ref = output_ref.mean()
loss_ref.backward()
weight_grad_ref = conv_layer.conv1.weight.grad.clone()
if (
conv_layer.conv1.bias is not None
and conv_layer.conv1.bias.grad is not None
):
bias_grad_ref = conv_layer.conv1.bias.grad.clone()
conv_layer.clear_gradients()
conv_layer.conv1.weight = original_weight
opt = paddle.optimizer.AdamW(
learning_rate=0.001, parameters=conv_layer.parameters()
)
mp_config = {"parallelize_plan": {"conv1": dist.ConvParallel()}}
parallel_config = {
"mp_config": mp_config,
}
dist_model, dist_opt = dist.parallelize(
conv_layer, opt, config=parallel_config
)
output_intermediate = dist_model(input_tensor)
loss_intermediate = paddle.mean(output_intermediate)
loss_intermediate.backward()
weight_grad_intermediate = dist_model.conv1.weight.grad.clone()
if (
dist_model.conv1.bias is not None
and dist_model.conv1.bias.grad is not None
):
bias_grad_intermediate = dist_model.conv1.bias.grad.clone()
def compare_tensors(name, tensor1, tensor2):
np.testing.assert_allclose(
tensor1.numpy(), tensor2.numpy(), rtol=1e-8, atol=1e-8
)
def compare_grads(name, grad1, grad2):
np.testing.assert_allclose(
grad1.numpy(), grad2.numpy(), rtol=1e-6, atol=1e-6
)
if data_format == "NCHW":
w_size = output_ref.shape[-1]
else:
w_size = output_ref.shape[-2]
if dist.get_rank() == 0:
start_index = 0
end_index = w_size // 2
else:
start_index = w_size // 2
end_index = w_size
if data_format == "NCHW":
compare_tensors(
"output",
output_ref[:, :, :, start_index:end_index],
output_intermediate._local_value(),
)
else:
compare_tensors(
"output",
output_ref[:, :, start_index:end_index, :],
output_intermediate._local_value(),
)
compare_grads("w", weight_grad_ref, weight_grad_intermediate)
if (
conv_layer.conv1.bias is not None
and conv_layer.conv1.bias.grad is not None
):
compare_grads("b", bias_grad_ref, bias_grad_intermediate)
def _test_conv_case(
self,
N,
C,
H,
W,
kernel_size,
padding,
bias_attr,
mesh,
test_name="conv_test",
dtype_str="float32",
data_format="NCHW",
stride=1,
):
self.set_seed(2025)
conv_layer = nn.Conv2D(
C,
C,
kernel_size=kernel_size,
padding=padding,
bias_attr=bias_attr,
data_format=data_format,
stride=stride,
)
original_weight = conv_layer.weight
conv_layer.weight = original_weight
if data_format == "NCHW":
input_tensor = paddle.randn([N, C, H, W])
shard_axis_input = 3
else:
input_tensor = paddle.randn([N, H, W, C])
shard_axis_input = 2
output_ref = conv_layer(input_tensor)
loss_ref = output_ref.mean()
loss_ref.backward()
weight_grad_ref = conv_layer.weight.grad.clone()
if conv_layer.bias is not None and conv_layer.bias.grad is not None:
bias_grad_ref = conv_layer.bias.grad.clone()
conv_layer.clear_gradients()
conv_layer.weight = original_weight
rank = dist.get_rank()
input_placements = [
dist.Replicate() for _ in range(len(mesh.dim_names))
]
mp_dim_index = mesh.dim_names.index("mp")
input_placements[mp_dim_index] = dist.Shard(shard_axis_input)
sharded_input = dist.shard_tensor(input_tensor, mesh, input_placements)
output_sharded = conv_layer(sharded_input)
loss_sharded = paddle.mean(output_sharded)
loss_sharded.backward()
weight_grad_shard = conv_layer.weight.grad.clone()
if conv_layer.bias is not None and conv_layer.bias.grad is not None:
bias_grad_shard = conv_layer.bias.grad.clone()
def compare_grads(name, grad1, grad2):
np.testing.assert_allclose(
grad1.numpy(), grad2.numpy(), rtol=1e-6, atol=1e-7
)
def compare_tensors(name, tensor1, tensor2):
np.testing.assert_allclose(
tensor1.numpy(), tensor2.numpy(), rtol=1e-8, atol=1e-8
)
if data_format == "NCHW":
w_size = output_ref.shape[-1]
else:
w_size = output_ref.shape[-2]
if dist.get_rank() == 0:
start_index = 0
end_index = w_size // 2
else:
start_index = w_size // 2
end_index = w_size
if data_format == "NCHW":
compare_tensors(
"output",
output_ref[:, :, :, start_index:end_index],
output_sharded._local_value(),
)
else:
compare_tensors(
"output",
output_ref[:, :, start_index:end_index, :],
output_sharded._local_value(),
)
compare_grads("w", weight_grad_ref, weight_grad_shard)
if conv_layer.bias is not None and conv_layer.bias.grad is not None:
compare_grads("b", bias_grad_ref, bias_grad_shard)
def run_test_cases(self):
mesh1 = dist.ProcessMesh([0, 1], dim_names=['mp'])
# ========= Case 1: padding > 0, stride = 1 =========
# Typical convolution with halo exchange required.
self._test_conv_case(
N=1,
C=10,
H=32,
W=32,
kernel_size=3,
padding=1,
bias_attr=True,
mesh=mesh1,
)
self._test_conv_case(
N=2,
C=8,
H=16,
W=32,
kernel_size=5,
padding=2,
bias_attr=False,
mesh=mesh1,
)
self._test_conv_case(
N=4,
C=6,
H=28,
W=28,
kernel_size=3,
padding=1,
bias_attr=True,
mesh=mesh1,
)
# NHWC format with padding > 0
self._test_conv_case(
N=2,
C=8,
H=16,
W=32,
kernel_size=3,
padding=1,
bias_attr=True,
mesh=mesh1,
data_format="NHWC",
)
self._test_conv_case(
N=4,
C=6,
H=28,
W=28,
kernel_size=5,
padding=2,
bias_attr=False,
mesh=mesh1,
data_format="NHWC",
)
# ========= Case 2: padding = 0, stride == kernel_size =========
# No halo exchange needed, input width must be divisible by stride.
self._test_conv_case(
N=1,
C=10,
H=32,
W=32,
kernel_size=1,
padding=0,
bias_attr=True,
mesh=mesh1,
stride=1,
)
self._test_conv_case(
N=4,
C=6,
H=32,
W=32,
kernel_size=2,
padding=0,
bias_attr=False,
mesh=mesh1,
stride=2,
)
self._test_conv_case(
N=2,
C=8,
H=16,
W=32,
kernel_size=4,
padding=0,
bias_attr=True,
mesh=mesh1,
stride=4,
)
# NHWC format with padding = 0
self._test_conv_case(
N=1,
C=10,
H=32,
W=32,
kernel_size=2,
padding=0,
bias_attr=True,
mesh=mesh1,
stride=2,
data_format="NHWC",
)
self._test_conv_case(
N=4,
C=6,
H=32,
W=32,
kernel_size=4,
padding=0,
bias_attr=False,
mesh=mesh1,
stride=4,
data_format="NHWC",
)
# ========= Case 3: 2D ProcessMesh (dp + tp) =========
mesh2 = dist.ProcessMesh([[0, 1]], dim_names=['dp', 'mp'])
# padding > 0
self._test_conv_case(
N=2,
C=8,
H=32,
W=32,
kernel_size=3,
padding=1,
bias_attr=True,
mesh=mesh2,
)
self._test_conv_case(
N=4,
C=6,
H=28,
W=28,
kernel_size=5,
padding=2,
bias_attr=False,
mesh=mesh2,
)
# padding = 0
self._test_conv_case(
N=2,
C=8,
H=16,
W=32,
kernel_size=1,
padding=0,
bias_attr=True,
mesh=mesh2,
stride=1,
)
# NHWC format, both padding > 0 and = 0
self._test_conv_case(
N=4,
C=6,
H=28,
W=28,
kernel_size=3,
padding=1,
bias_attr=True,
mesh=mesh2,
data_format="NHWC",
)
self._test_conv_case(
N=1,
C=10,
H=32,
W=32,
kernel_size=4,
padding=0,
bias_attr=True,
mesh=mesh2,
stride=4,
data_format="NHWC",
)
self._test_intermediate(
N=1,
C=10,
H=32,
W=32,
kernel_size=3,
padding=1,
bias_attr=True,
mesh=mesh1,
)
self._test_intermediate(
N=1,
C=10,
H=32,
W=32,
kernel_size=2,
padding=0,
bias_attr=True,
mesh=mesh1,
stride=2,
data_format="NHWC",
)
self._test_intermediate(
N=4,
C=6,
H=28,
W=28,
kernel_size=3,
padding=1,
bias_attr=True,
mesh=mesh2,
data_format="NHWC",
)
if __name__ == '__main__':
tester = TestTPConv()
tester.run_test_cases()