[ONNX] CSE pass in export pollutes Scope information
Labels: module: onnx, triaged, module: regression, bug

### 🐛 Describe the bug

## Description

There are two issues found with recently adding CSE pass to ONNX export.

1. CSE pass does not update Scope information. Common subexpressions from different modules are eliminated while not updating scope information for the node that is left. The result is that it appears the node's 'users' from other modules are accessing internal information from the module where the node belongs to.
2. Some ONNX passes are re-creating duplicated Constants after the CSE pass.

Both issues are illustrated in below example.

```python
import torch


class M(torch.nn.Module):
    def __init__(self, bias):
        super().__init__()
        self.bias = bias

    def forward(self, x):
        return x + self.bias


class N(torch.nn.Module):
    def __init__(self, layers: int = 3):
        super().__init__()
        # 'bias' is same value for all layers, hence common sub expression.
        self.layers = torch.nn.ModuleList(
            [M(bias=torch.tensor([1.0])) for i in range(layers)]
        )

    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x


x = torch.randn(8192, 1, 1)
model = N()

torch.onnx.export(
    model,
    x,
    "model.onnx",
    verbose=True,
    opset_version=15,
    input_names=["x"],
    dynamic_axes={"x": [0]},
)

```

Observe ONNX graph:
```python
Exported graph: graph(%x : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu)):
  %/layers.0/Constant_output_0 : Float(1, strides=[1], requires_grad=0, device=cpu) = onnx::Constant[value={1}, onnx_name="/layers.0/Constant"](), scope: __main__.N::/__main__.M::layers.0 # repro_oct_cse.py:101:0
  %/layers.0/Add_output_0 : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu) = onnx::Add[onnx_name="/layers.0/Add"](%x, %/layers.0/Constant_output_0), scope: __main__.N::/__main__.M::layers.0 # repro_oct_cse.py:101:0
  %/layers.0/Constant_1_output_0 : Float(1, strides=[1], requires_grad=0, device=cpu) = onnx::Constant[value={1}, onnx_name="/layers.0/Constant_1"](), scope: __main__.N::/__main__.M::layers.0 # repro_oct_cse.py:101:0
  %/layers.1/Add_output_0 : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu) = onnx::Add[onnx_name="/layers.1/Add"](%/layers.0/Add_output_0, %/layers.0/Constant_1_output_0), scope: __main__.N::/__main__.M::layers.1 # repro_oct_cse.py:101:0
  %/layers.0/Constant_2_output_0 : Float(1, strides=[1], requires_grad=0, device=cpu) = onnx::Constant[value={1}, onnx_name="/layers.0/Constant_2"](), scope: __main__.N::/__main__.M::layers.0 # repro_oct_cse.py:101:0
  %6 : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu) = onnx::Add[onnx_name="/layers.2/Add"](%/layers.1/Add_output_0, %/layers.0/Constant_2_output_0), scope: __main__.N::/__main__.M::layers.2 # repro_oct_cse.py:101:0
  return (%6)
```

The same constant value `onnx::Constant[value={1}]` still appeared 3 times,
`%/layers.0/Constant_output_0`, `%/layers.0/Constant_1_output_0`, `%/layers.0/Constant_2_output_0`.
And the Scope information is incorrect. All 3 constants are considered belonging to `layers.0`, where as
they should be considered belonging to `layers.0`, `layers.1` and `layers.2` respectively.
This **breaks** node naming and local function export as both depends on Scope information to figure out nodes' original module.

For comparison, this is the graph with cse disabled during export
```python
Exported graph: graph(%x : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu)):
  %/layers.0/Constant_output_0 : Float(1, strides=[1], requires_grad=0, device=cpu) = onnx::Constant[value={1}, onnx_name="/layers.0/Constant"](), scope: __main__.N::/__main__.M::layers.0 # repro_oct_cse.py:101:0
  %/layers.0/Add_output_0 : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu) = onnx::Add[onnx_name="/layers.0/Add"](%x, %/layers.0/Constant_output_0), scope: __main__.N::/__main__.M::layers.0 # repro_oct_cse.py:101:0
  %/layers.1/Constant_output_0 : Float(1, strides=[1], requires_grad=0, device=cpu) = onnx::Constant[value={1}, onnx_name="/layers.1/Constant"](), scope: __main__.N::/__main__.M::layers.1 # repro_oct_cse.py:101:0
  %/layers.1/Add_output_0 : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu) = onnx::Add[onnx_name="/layers.1/Add"](%/layers.0/Add_output_0, %/layers.1/Constant_output_0), scope: __main__.N::/__main__.M::layers.1 # repro_oct_cse.py:101:0
  %/layers.2/Constant_output_0 : Float(1, strides=[1], requires_grad=0, device=cpu) = onnx::Constant[value={1}, onnx_name="/layers.2/Constant"](), scope: __main__.N::/__main__.M::layers.2 # repro_oct_cse.py:101:0
  %6 : Float(*, 1, 1, strides=[1, 1, 1], requires_grad=0, device=cpu) = onnx::Add[onnx_name="/layers.2/Add"](%/layers.1/Add_output_0, %/layers.2/Constant_output_0), scope: __main__.N::/__main__.M::layers.2 # repro_oct_cse.py:101:0
  return (%6)
```

## Proposal
CSE to update scope with common ancestor, whenever it finds two nodes of common sub expression.

For above example, the scopes of the 3 nodes are
```
scope: __main__.N::/__main__.M::layers.0
scope: __main__.N::/__main__.M::layers.1
scope: __main__.N::/__main__.M::layers.2
```
Hence, they have common ancestor `scope: __main__.N::`. Ideally the node should be moved upward in graph to reflect the structural changes as well.

## Alternative Solution
ONNX exporter to run additional pass following CSE to update scope information based on user scopes.

### Versions

PyTorch master:

torch                     1.13.0a0+git585452b

cc @thiagocrepaldi @AllenTiTaiWang