Skip to content

Add torch.normal and Tensor.normal_ to the PyTorch converter - #2878

Open
GodfredArhinful wants to merge 1 commit into
apple:mainfrom
GodfredArhinful:torch-normal
Open

GodfredArhinful wants to merge 1 commit into
apple:mainfrom
GodfredArhinful:torch-normal

Conversation

@GodfredArhinful

Copy link
Copy Markdown

Fixes #1528

Adds a converter for torch.normal and the in-place Tensor.normal_. It follows the suggestion in #1528 to build on mb.random_normal the same way torch.rand builds on mb.random_uniform.

Why

normal_ is how StyleGAN2-style generators inject per-pixel noise. This is NoiseInjection from rosinality/stylegan2-pytorch, which GFPGAN uses unchanged (mentioned in #1528):

class NoiseInjection(nn.Module):
    def __init__(self):
        super().__init__()
        self.weight = nn.Parameter(torch.zeros(1))

    def forward(self, image, noise=None):
        if noise is None:
            batch, _, height, width = image.shape
            noise = image.new_empty(batch, 1, height, width).normal_()
        return image + self.weight * noise

On main, converting a conv block with this module fails with PyTorch convert function for op 'normal_' not implemented. With this change it converts, and the injected noise has mean -0.008 and std 1.003. #1528 also reports the same error for a GPT model.

What's covered

Overload TorchScript kind torch.export kind Output shape
Tensor.normal_(mean=0, std=1) normal_ normal_functional shape of self
normal.Tensor_float(mean, std=1) normal (3 inputs) normal.tensor_float broadcast of mean and std
normal.float_Tensor(mean, std) normal (3 inputs) normal.float_tensor broadcast of mean and std
normal.Tensor_Tensor(mean, std) normal (3 inputs) normal.tensor_tensor broadcast of mean and std
normal.float_float(mean, std, size) normal (8 inputs) normal.float_float size, honoring dtype

When mean and std are scalars known at conversion time, they go straight into random_normal's mean and stddev. Otherwise the converter scales and shifts a standard normal sample. A dynamic size, which TorchScript passes as a list of scalars, is concatenated into a shape tensor. A 0-d result is sampled as one value and squeezed. As with rand and randn, the generator argument is ignored.

Tests

All 48 new tests fail on main with not implemented errors and pass with this change.

TestNormal can't compare random values directly. Each model instead returns 1.0 when the mean and standard deviation of a 16,384-value sample land within 0.1 standard deviations of their expected values. That margin is more than 12 standard errors wide, so the check stays strict even though the fp16 backend loosens atol. The tests cover:

The ExecuTorch cases are marked xfail. PyTorch's edge verifier rejects these ops because they are not in the Core ATen opset, the same situation as the existing randn_like test.

Ran locally on macOS 26.6.1 (arm64) with torch 2.8.0. All TestNormal cases pass on TorchScript and torch.export, for both the mlprogram and neuralnetwork backends. I also ran the rest of the torch frontend tests on this branch and on main. The results are identical: a few TestPad::test_pad_constant and TestConv cases fail or crash on this machine with or without this change.

Converts every overload of torch.normal (Tensor_float, float_Tensor,
Tensor_Tensor, float_float) and the in-place Tensor.normal_, which
torch.export lowers to normal_functional. Scalar mean and std known at
conversion time map onto mb.random_normal directly. Tensor or runtime
parameters shift and scale a standard normal sample over the broadcast
shape of mean and std.

Fixes apple#1528

@register_torch_op(
torch_alias=[
"normal_functional",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are your unit tests testing each of these five alias, as well as normal?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Every test is parametrized over both the TorchScript and torch.export frontends, and together they reach every registered name:

  • normal (TorchScript, all out-of-place overloads): test_normal_overloads (all four), test_normal_broadcast_shape, test_normal_constant_tensor_parameters, test_normal_dynamic_shape
  • normal_ (TorchScript in-place, resolved to normal) and normal_functional (torch.export in-place): test_normal_inplace, test_normal_dynamic_shape[inplace]
  • normal.tensor_float: test_normal_overloads[Tensor_float]
  • normal.float_tensor: test_normal_overloads[float_Tensor]
  • normal.tensor_tensor: test_normal_overloads[Tensor_Tensor], test_normal_broadcast_shape, test_normal_constant_tensor_parameters, test_normal_dynamic_shape[Tensor_Tensor]
  • normal.float_float: test_normal_overloads[float_float], test_normal_dynamic_shape[float_float]

This was confirmed by wrapping the registered converter and logging node.kind while running TestNormal on both frontends.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants