README.md
1# Guidance for Operator Developer
2
3PyTorch’s operators sometimes require changes for different reasons (e.g. from improving their usability to fixing bugs). These changes can be backward compatibility (BC) breaking, where older programs will no longer run as expected (or at all) on the latest version of PyTorch (an old program / new runtime problem), or forward compatibility (FC) breaking, where new programs will not run on older versions of PyTorch (a new program / old runtime problem). This guidance focuses on the requirements for maintaining backwards compatibility when making changes to an operator.
4In order to do this we introduce the concept of the *upgrader*: a method to adapt the new operator to mimic the old operator behavior.
5When a new runtime reads an old program containing the old operator definition, the upgrader will adapt the old operator definition to comply with the new operator implementation. As you would expect, an upgrader is only applied when an old operation definition is encountered (i.e. if there are no "old" operators in the program, no upgrader would be used).
6For more details on the reasoning behind this new requirement please refer to the [PyTorch Operator Versioning RFC](https://github.com/pytorch/rfcs/blob/master/RFC-0017-PyTorch-Operator-Versioning.md).
7
8If the change to the operator is BC-breaking in either the schema or the semantics, you are responsible for writing an upgrader to prevent the change from becoming BC breaking.
9
10You can determine if your change in the operator is BC breaking, if it fails `test/forward_backward_compatibility/check_forward_backward_compatibility.py `.
11
12### Some examples BC breaking changes
13
14When making changes to the operators, the first thing to identify is if it's BC/FC breaking. Again, we only targetting for BC breaking changes on this guidance. Here are some examples to help understanding what a BC changes may look like:
15
16#### Backward Compatibility Breakage:
17
18- Return types are more generic than the older version
19 - Old: `foo(Tensor self, int a) -> int`
20 - New: `foo(Tensor self, int a) -> Scalar`
21- Argument types are more specific than the older version
22 - Old: `foo(Tensor self, Scalar a) -> int`
23 - New: `foo(Tensor self, int a) -> int`
24- Added new arguments don’t have associated default values
25 - Old: `foo(Tensor self, int a) -> int`
26 - New: `foo(Tensor self, int a, int b) -> int`
27- Internal implementation change even when the schema remains the same
28- Deprecating an operator
29
30
31### The steps to write upgrader:
32
33### 1.Preparation
34
35[Build PyTorch from souce](https://github.com/pytorch/pytorch#from-source) and prepare a test model before making changes to the operator, following the process below. A test model before making the operator changes is needed to test the upgrader. Otherwise, after the change to operator, the new runtime will no longer be able to produce a model with the historic operator and can't test it anymore.
36
37 1. Add a test module in `test/jit/fixtures_srcs/fixtures_src.py`. In `test/jit/fixtures_srcs/generate_models.py`,
38 ```
39 class TestVersionedLinspaceV7(torch.nn.Module):
40 def __init__(self) -> None:
41 super().__init__()
42
43 def forward(self, a: Union[int, float, complex], b: Union[int, float, complex]):
44 c = torch.linspace(a, b, steps=5)
45 d = torch.linspace(a, b)
46 return c, d
47 ```
48 Please make sure the module uses the changed operator and follow the name schema ` TestVersioned{${OpnameOverloadedname}}V${kProducedFileFormatVersion}`. [`kProducedFileFormatVersion`](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h#L82) can be found in `versions.h`. The example operator usage can be found on [PyTorch Docs](https://pytorch.org/docs/stable/index.html), like [linspace operator](https://pytorch.org/docs/stable/generated/torch.linspace.html)
49 2. Register its corresponding changed operator in ALL_MODULES like following. Use an instance as the key and the changed operator as the value. It will ensure the test model covers everything needed. It's important to check in a valid test model before making the change to the runtime, as it will be really challenging to switch to the revision of the source code and regenerate the test model after the change is merged.
50
51 ```
52 # key: test module instance, value: changed operator name
53 ALL_MODULES = {
54 TestVersionedLinspaceV7(): "aten::linspace",
55 }
56 ```
57
58 This module should include the changed operator. If the operator isn't covered in the model, the model export process will fail.
59
60 3. Export the model to `test/jit/fixtures` by running
61 ```
62 python test/jit/fixtures_src/generate_models.py
63 ```
64
65 4. Commit the change and submit a pull request.
66
67### 2. Make changes to the operator and write an upgrader.
68 1. Make the operator change.
69 2. Write an upgrader in `torch/csrc/jit/operator_upgraders/upgraders_entry.cpp` file inside a map `kUpgradersEntryMap`. The softly enforced naming format is `<operator_name>_<operator_overload>_<start>_<end>`. The start and end means the upgrader can be applied to the operator exported during when [the global operator version](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h#L82) within the range `[start, end]`. Let's take an operator `linspace` with the overloaded name `out` as an example. The first thing is to check if the upgrader exists in [upgraders_entry.cpp](https://github.com/pytorch/pytorch/blob/master/torch/csrc/jit/operator_upgraders/upgraders_entry.cpp).
70 1. If the upgrader doesn't exist in `upgraders_entry.cpp`, the upgrader name can be `linspace_out_0_{kProducedFileFormatVersion}`, where [`kProducedFileFormatVersion`](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h#L82) can be found in [versions.h](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h).
71 2. If the upgrader exist in `upgraders_entry.cpp`, for example `linspace_out_0_7` (means `linspace.out` operator is changed when operator version is bumped from 7 to 8),
72 1. If it's possible to write an upgrader valid for `linspace` before versioning bumping to 8, after versioning bumping to 8, write an upgrader `linspace_out_0_{kProducedFileFormatVersion}`
73 2. If it's impossible to write an upgrader valid for `linspace` before versioning bumping to 8, check the date when the version is bumped to 8 at [`versions.h`](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h#L82). If it has been 180 days, write an upgrader `linspace_out_8_{kProducedFileFormatVersion}` for `linspace.out` after bumping to 8, and deprecate the old upgrader. If it hasn't been 180 days, wait until 180 days and do the same changes as above.
74
75 To write an upgrader, you would need to know how the new runtime with the new `linspace` operator can handle an old model with the old `linspace` operator. When `linspace` is bumped to 8, the change is to make `step` a required argument, instead of an optional argument. The old schema is:
76 ```
77 linspace(start: Union[int, float, complex], end: Union[int, float, complex], steps: Optional[int], dtype: Optional[int], layout: Optional[int],
78 device: Optional[Device], pin_memory: Optional[bool]):
79 ```
80 And the new schema is:
81 ```
82 linspace(start: Union[int, float, complex], end: Union[int, float, complex], steps: int, dtype: Optional[int], layout: Optional[int],
83 device: Optional[Device], pin_memory: Optional[bool]):
84 ```
85 An upgrader will only be applied to an old model and it won't be applied to a new model. The upgrader can be written with the following logic:
86 ```
87 def linspace_0_7(start: Union[int, float, complex], end: Union[int, float, complex], steps: Optional[int], *, dtype: Optional[int], layout: Optional[int],
88 device: Optional[Device], pin_memory: Optional[bool]):
89 if (steps is None):
90 return torch.linspace(start=start, end=end, steps=100, dtype=dtype, layout=layout, device=device, pin_memory=pin_memory)
91 return torch.linspace(start=start, end=end, steps=steps, dtype=dtype, layout=layout, device=device, pin_memory=pin_memory)
92 ```
93
94 The actual upgrader needs to be written as [TorchScript](https://pytorch.org/docs/stable/jit.html), and the below example is the actual upgrader of the operator `linspace.out `and the operator ` linspace` exported at version from 0 to 7.
95 ```
96 static std::unordered_map<std::string, std::string> kUpgradersEntryMap(
97 {
98 {"linspace_0_7", R"SCRIPT(
99 def linspace_0_7(start: Union[int, float, complex], end: Union[int, float, complex], steps: Optional[int], *, dtype: Optional[int], layout: Optional[int],
100 device: Optional[Device], pin_memory: Optional[bool]):
101 if (steps is None):
102 return torch.linspace(start=start, end=end, steps=100, dtype=dtype, layout=layout, device=device, pin_memory=pin_memory)
103 return torch.linspace(start=start, end=end, steps=steps, dtype=dtype, layout=layout, device=device, pin_memory=pin_memory)
104 )SCRIPT"},
105 }
106 ```
107 With the upgrader, when a new runtime loads an old model, it will first check the operator version of the old model. If it's older than the current runtime, it will replace the operator from the old model with the upgrader above.
108
109 3. Bump [`kMaxSupportedFileFormatVersion`](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h#L15) the [`kProducedFileFormatVersion`](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h#L82) by 1 and provide the reasons under [`versions.h`](https://github.com/pytorch/pytorch/blob/master/caffe2/serialize/versions.h#L73-L81)
110 ```
111
112 constexpr uint64_t kMaxSupportedFileFormatVersion = 0x9L;
113
114 ...
115 // We describe new operator version bump reasons here:
116 // 1) [01/24/2022]
117 // We bump the version number to 8 to update aten::linspace
118 // and aten::linspace.out to error out when steps is not
119 // provided. (see: https://github.com/pytorch/pytorch/issues/55951)
120 // 2) [01/30/2022]
121 // Bump the version number to 9 to update aten::logspace and
122 // and aten::logspace.out to error out when steps is not
123 // provided. (see: https://github.com/pytorch/pytorch/issues/55951)
124 constexpr uint64_t kProducedFileFormatVersion = 0x9L;
125 ```
126
127 4. In `torch/csrc/jit/operator_upgraders/version_map.cpp`, add changes like below. You will need to make sure that the entry is **SORTED** by the bumped to version number.
128 ```
129 {{${operator_name.overloaded_name},
130 {{${bump_to_version},
131 "${upgrader_name}",
132 "${old operator schema}"}}},
133 ```
134 For the example operator `linspace`, if there are two version bumps, one is bumped to 8 and one is bumped to 12, the sorted result is:
135 ```
136 {{"aten::linspace",
137 {{12,
138 "linspace_0_11",
139 "aten::linspace(Scalar start, Scalar end, int? steps=None, *, ScalarType? dtype=None, Layout? layout=None, Device? device=None, bool? pin_memory=None) -> Tensor"}}},
140 {{8,
141 "linspace_0_7",
142 "aten::linspace(Scalar start, Scalar end, int? steps=None, *, ScalarType? dtype=None, Layout? layout=None, Device? device=None, bool? pin_memory=None) -> Tensor"}}},
143 ```
144
145 5. After [rebuilding PyTorch](https://github.com/pytorch/pytorch#from-source), run the following command to auto update the file [`torch/csrc/jit/mobile/upgrader_mobile.cpp`](https://github.com/pytorch/pytorch/blob/8757e21c6a4fc00e83539aa7f9c28eb11eff53c1/torch/csrc/jit/mobile/upgrader_mobile.cpp). After rebuild PyTorch from source (`python setup.py`), run
146
147 ```
148 python pytorch/torchgen/operator_versions/gen_mobile_upgraders.py
149 ```
150
151 6. Add a test. With the model generated from step 1, you will need to add tests in `test/test_save_load_for_op_versions.py`. Following is an example to write a test
152
153 ```
154 @settings(max_examples=10, deadline=200000) # A total of 10 examples will be generated
155 @given(
156 sample_input=st.tuples(st.integers(min_value=5, max_value=199), st.floats(min_value=5.0, max_value=199.0))
157 ) # Generate a pair (integer, float)
158 @example((2, 3, 2.0, 3.0)) # Ensure this example will be covered
159 def test_versioned_div_scalar(self, sample_input):
160 # Step 1. Write down the old behavior of this operator, if possible
161 def historic_div_scalar_float(self, other: float):
162 return torch.true_divide(self, other)
163
164 # Step 2. Write down how current module should look like
165 class MyModuleFloat(torch.nn.Module):
166 def __init__(self) -> None:
167 super().__init__()
168
169 def forward(self, a, b: float):
170 return a / b
171 try:
172 # Step 3. Load the old model and it will apply upgrader
173 v3_mobile_module_float = _load_for_lite_interpreter(
174 pytorch_test_dir + "/jit/fixtures/test_versioned_div_scalar_float_v2.ptl")
175 v3_server_module_float = torch.jit.load(
176 pytorch_test_dir + "/jit/fixtures/test_versioned_div_scalar_float_v2.ptl")
177 except Exception as e:
178 self.skipTest("Failed to load fixture!")
179
180 # Step4. Load the new model and it won't apply the upgrader
181 current_mobile_module_float = self._save_load_mobile_module(MyModuleFloat)
182 current_server_module_float = self._save_load_module(MyModuleFloat)
183
184 for val_a, val_b in product(sample_input, sample_input):
185 a = torch.tensor((val_a,))
186 b = val_b
187
188 def _helper(m, fn):
189 m_result = self._try_fn(m, a, b)
190 fn_result = self._try_fn(fn, a, b)
191
192 if isinstance(m_result, Exception):
193 self.assertTrue(fn_result, Exception)
194 else:
195 self.assertEqual(m_result, fn_result)
196
197 # Ensure the module loaded from the old model with upgrader
198 # has the same result as the module loaded from the new model
199 _helper(v3_mobile_module_float, current_mobile_module_float)
200 _helper(v3_mobile_module_float, current_server_module_float)
201
202 # Ensure the module loaded from the new model with upgrader
203 # has the same result as the module loaded from the new model
204 _helper(current_mobile_module_float, torch.div)
205 _helper(current_server_module_float, torch.div)
206 ```
207
208 7. Commit all changes made in step 2 in a single pull request and submit it.
209
210You can look at following PRs to get the rough idea of what needs to be done:
2111. [PR that adds `logspace` test modules](https://github.com/pytorch/pytorch/pull/72052)
2122. [PR that updates `logspace`](https://github.com/pytorch/pytorch/pull/72051)
213
214---
215**NOTE**
216
2171. Adding arguments with a default value to an operator is not BC breaking, and thus does not require an upgrader. For example, the following change to operator `foo` is backwards compatible:
218```
219# before
220def foo(x, y):
221 return x, y
222```
223```
224# after
225def foo(x, y, z=100):
226 return x, y, z
227```
228
2292. To help understanding the BC/FC breakage changes, here are some FC breaking changes examples. The solution to resolve it is not there yet. If it's desired, please report it in either [PyTorch Forum](https://discuss.pytorch.org/) or [PyTorch GitHub](https://github.com/pytorch/pytorch). We will prioritize it accordingly.
230
231 - Adding new default argument:
232 - Adding a new default argument not RIGHT BEFORE the out arguments which can be 0 or more.
233 - Old: `foo(Tensor self, int a, int b=1, Tensor(a!) out) -> (Tensor(a!))`
234 - New: `foo(Tensor self, int a, int c=1, int b=1, Tensor(a!) out) -> (Tensor(a!))`
235
236 - Adding out argument NOT at the end of the schema.
237 - Old: `foo(Tensor self, int a, int b=1, Tensor(a!) out) -> (Tensor(a!))`
238 - New: `foo(Tensor self, int a, Tensor(d!), int b=1, Tensor(a!) out) -> (Tensor(a!), Tensor(d!))`
239
240 - Adding default arguments with container types such as ListType or DictType (list or dict).
241 - Old: `foo(Tensor self, int a, int b=1, Tensor(a!) out) -> (Tensor(a!))`
242 - New: `foo(Tensor self, int a, int b=1, int[2] c=1, Tensor(a!) out) -> (Tensor(a!))`
243 - Changing default argument’s name
244 - This will only work when the default argument always uses the default value (so that serialization will ignore it). In all other cases, it will fail.
245 - Old: `foo(Tensor self, int a, int b=1, Tensor(a!) out) -> (Tensor(a!))`
246 - New: `foo(Tensor self, int a, int c=1, Tensor(a!) out) -> (Tensor(a!))`
247 - Changing default argument’s default value. This will break when this argument is saved with the default value in newer runtime. Older runtime will use its old default value which will lead to wrong output.
248 - Old: `foo(Tensor self, int a, int b=1, Tensor(a!) out) -> (Tensor(a!))`
249 - New: `foo(Tensor self, int a, int b=4, Tensor(a!) out) -> (Tensor(a!))`
250 - Adding new operator
251
252---
253