Skip to content

Commit 73e791e

Browse files
author
ematejska
authored
Merge pull request #415 from monicadsong/master
RFC: SavedModel Fingerprinting
2 parents eff9c82 + d62c5ab commit 73e791e

File tree

3 files changed

+300
-0
lines changed

3 files changed

+300
-0
lines changed
+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# SavedModel Fingerprinting
2+
3+
| Status | Accepted |
4+
:-------------- |:---------------------------------------------------- |
5+
| **RFC #** | 415 |
6+
| **Author(s)** | Monica Song ([email protected]) |
7+
| **Sponsor** | Cesar Crusius ([email protected]) |
8+
| **Updated** | 2022-06-10 |
9+
10+
## Objective
11+
12+
Following the convention of other types of electronic files and artifacts, the SavedModel format would benefit from having a fingerprint that uniquely identify the program it serializes.
13+
14+
## Motivation
15+
16+
SavedModels represent machine learning models in protocol buffer format. These machine learning models are composed of the TensorFlow graph defining the computation and the values of the weights of the nodes (a.k.a. the checkpoint).
17+
18+
Many users of TensorFlow have requested a fingerprint in the SavedModel format, to enable them the ability to track the usage and lineage of their models in research and production. Since the SavedModel contains several parts (the graphdef, the functiondef library, the checkpoint, etc) that are not all used in the loaded program, this design will provide a fingerprint consisting of multiple parts as well. Clients are free to pick and choose which parts of the fingerprint they want to use to meet their needs.
19+
20+
21+
## User Benefit
22+
23+
SavedModel fingerprinting has been requested by many users to enable them to track their impact by counting the number of SavedModels using their service or by counting the number of SavedModels in production that they created.
24+
25+
Clients of the SavedModel fingerprinting feature should understand how they consume SavedModel protobufs to identify and select which parts of the fingerprint fit their use cases. For example, some serving infrastructure is in C++ and relies on the SignatureDef to figure out what to load; thus, they would ignore the part of the fingerprint that describes the ObjectGraphDef which is only used in Python.
26+
27+
This document proposes a fingerprint that is comprised of a set of hashes, each with various guarantees to serve the requirements of our users.
28+
29+
## What is SavedModel Fingerprinting
30+
31+
By [definition](https://en.wikipedia.org/wiki/Fingerprint_(computing)), a fingerprint maps a large amount of data to a much shorter bit string. In this case, our goal is to map the serialized contents of the SavedModel protobuf to a set of 64-bit hashes. We choose 64 bits because fingerprints are typically 64 bits long and the consequences of collision are not so terrible that we need to make it 128 bits.
32+
33+
**The SavedModel fingerprint contains 5 parts total:**
34+
35+
1. **A File Checksum:** This is a hash of the contents of the `saved_model.pb`
36+
2. **Program hash of the GraphDef**
37+
3. **Program hash of the SignatureDef**
38+
4. **Program hash of the ObjectGraphDef**
39+
5. **Hash of checkpoint file**
40+
41+
The rationale for this selection is that:
42+
- The SignatureDef defines the necessary computation in C++
43+
- Serving in C++ for inference uses the SignatureDef, which is determined by the `signatures` options of the `tf.saved_model.save` API.
44+
- GraphDef has the FunctionDefLibrary
45+
- ObjectGraphDef is very useful when loading in Python (however, it is ignored in C++)
46+
- The checkpoint contains the weights of the model.
47+
48+
### Properties
49+
50+
51+
When two SavedModels have the same file checksum, this means that they are identical in their persisted format on disk, provided there is no collision.
52+
53+
When two SavedModels have all 3 identical program hashes, they are said to be **isomorphic.** This means their data in the SavedModel protobuf that is persisted on disk could be different, however they describe the same computation in their graphs. So, in effect, the program should behave the same. This is why it is called a “program hash.”
54+
55+
In order to make a program hashes, we have to **regularize** (a.k.a. **canonicalize**) the protobuf to remove the sources of non-determinism that could result in two computationally equivalent models having different contents in the SavedModel.
56+
57+
For example, when we try to serialize the following model in two different programs:
58+
```
59+
x = tf.Module()
60+
x.v = tf.Variable(1.0)
61+
tf.saved_model.save(x, model_path)
62+
```
63+
64+
we see that the names of the functions change as well as their order in the FunctionDefLibrary:
65+
66+
![Diff1](20220610-saved-model-fingerprinting/diff1.png)
67+
68+
![Diff2](20220610-saved-model-fingerprinting/diff2.png)
69+
70+
71+
In the above example, the two models should have **different** file checksums but **identical** program hashes.
72+
73+
The reason we have a file checksum and a regularized/canonicalized set of program hashes is to allow users the ability to select which ones to use for their use cases.
74+
75+
76+
<table>
77+
<tr>
78+
<td><strong>Part of Fingerprint</strong>
79+
</td>
80+
<td><strong>Use case</strong>
81+
</td>
82+
<td><strong>Deterministic?</strong>
83+
</td>
84+
<td><strong>Content-based?</strong>
85+
</td>
86+
<td><strong>Unique?</strong>
87+
</td>
88+
<td><strong>Useful for:</strong>
89+
</td>
90+
</tr>
91+
<tr>
92+
<td>File Checksum
93+
</td>
94+
<td>Enable tracking of the specific savedmodel
95+
</td>
96+
<td>No
97+
</td>
98+
<td>Yes
99+
</td>
100+
<td>Yes
101+
</td>
102+
<td>Tracking specific protobufs in production, such as those available on TF Hub
103+
</td>
104+
</tr>
105+
<tr>
106+
<td>Regularized hashes
107+
</td>
108+
<td>Enable tracking of the source code
109+
</td>
110+
<td>Yes
111+
</td>
112+
<td>Yes
113+
</td>
114+
<td>Yes
115+
</td>
116+
<td>Library authors who want to know who is using their code
117+
</td>
118+
</tr>
119+
</table>
120+
121+
122+
## Comparison of the File Checksum and Program Hashes
123+
124+
What parts of the fingerprint change?
125+
126+
127+
<table>
128+
<tr>
129+
<td><strong>Case</strong>
130+
</td>
131+
<td><strong>File Checksum</strong>
132+
</td>
133+
<td><strong>Program Hashes</strong>
134+
</td>
135+
</tr>
136+
<tr>
137+
<td>Model A and Model B are produced by calling `tf.saved_model.save(model, “/tmp)` function twice in a row in the same process.
138+
</td>
139+
<td>Different
140+
</td>
141+
<td>Same
142+
</td>
143+
</tr>
144+
<tr>
145+
<td>Model A and Model B are produced by the exact same program using `bazel run`.
146+
</td>
147+
<td>Different
148+
</td>
149+
<td>Same
150+
</td>
151+
</tr>
152+
<tr>
153+
<td>Model B is a copy of Model A (e.g. you ctrl-C, ctrl-V the file).
154+
</td>
155+
<td>Same
156+
</td>
157+
<td>Same
158+
</td>
159+
</tr>
160+
</table>
161+
162+
163+
## Design Proposal
164+
165+
The engineering impact of this project is significant in that it adds an additional file (`fingerprint.pb`) to the SavedModel format.
166+
167+
168+
### A Separate File for the Fingerprint
169+
170+
We plan to add an additional protobuf in the SavedModel directory:
171+
172+
* `assets/`
173+
* `saved_model.pb`
174+
* **<code>fingerprint.pb</code>**
175+
* <code>variables/</code>
176+
177+
The advantages of having a separate file for the fingerprint are:
178+
- It neatly encapsulates the changes that this design proposes
179+
- It does not interfere with the MetaGraphDef in the saved_model.pb. The MetaGraphDef protobuf is already extremely large so we want to avoid adding fields to it.
180+
181+
One drawback of having a separate file is that it is a potentially significant change to the SavedModel format and could break any users who are using the SavedModel format in ways not endorsed by our library.
182+
183+
### API for reading and writing the fingerprints
184+
185+
All fingerprints will be written to disk automatically, and there is no way to bypass or customize writing fingerprints when serializing a SavedModel.
186+
187+
To read the fingerprint, users can load the protobuf into memory and read the field. At first, there will not be dedicated APIs in Python to get the fingerprint, since no other part of the SavedModel has special APIs to access its value. However, if the need arises, we can add APIs under the `tf.saved_model` namespace to read the fingerprint.
188+
189+
### Validation
190+
191+
When loading the SavedModel, we will log a warning (absl logging) if the fingerprint does not match the contents of `saved_model.pb`.
192+
193+
It should not be possible to create an invalid fingerprint if clients use our APIs correctly. The fingerprint and the SavedModel
194+
should always be in sync. If there’s a mismatch, then we assume that someone has changed the `fingerprint.pb` or `saved_model.pb` in ways not endorsed by TensorFlow.
195+
196+
The SavedModel CLI is also a good place for verification as well. We can check that the fingerprint is valid for the corresponding `saved_model.pb` file, based on an option in the CLI. We can display (in colored text) the result of the match.
197+
198+
We can also explore the option of adding a Python API such as `tf.saved_model.check_fingerprint(file_name)` to enable clients to validate the SavedModel.
199+
200+
### Implementation Details
201+
202+
The fingerprinting will be added as a final step of serialization. The fingerprint infrastructure will take as input the MetaGraphDef and hash each component of it, doing the necessary regularization as well.
203+
204+
The hashing will occur in C++, since it is easier to find an open source hashing algorithm in C++ than in Python. There will be pybinded wrappers for access from Python. This will require passing the protobuf across the Python and C++ boundary which may require an extra cycle of de/serialization.
205+
206+
### Launch Plan
207+
208+
**We will implement fingerprinting behind a flag such that it will not introduce any behavioral changes.**
209+
210+
Once all testing has been done, writing the fingerprint protobuf will be the default behavior for all SavedModels written.
211+
212+
213+
### Alternatives Considered
214+
215+
216+
#### Modifying the MetaGraphDef
217+
We considered adding another section to the MetaGraphDef. There are currently seven parts of the MetaGraphDef (Metainfodef, GraphDef, SaverDef, CollectionDef, SignatureDef, AssetFileDef, ObjectGraphDef). This alternative proposed adding a FingerprintDef as the 8th section of the MetaGraphDef. However, we wanted to avoid cluttering the MetaGraphDef any further.
218+
219+
We also considered tagging the parts of the MetaGraphDef with hashes. This idea also adds unwanted complexity to the MetaGraphDef.
220+
221+
#### Not persisting fingerprint on disk
222+
223+
We considered only providing APIs for reading and calculating on the fly the fingerprint from the serialized protobuf, as opposed to storing them in a separate file.
224+
225+
Advantages:
226+
- No changes to the format.
227+
- Always “correct”
228+
229+
Disadvantages:
230+
- No verification possible since the fingerprint is always computed on the fly.
231+
- Extra time to compute.
232+
- Need to provide an API to allow clients to use.
233+
234+
235+
#### Python Protocol Method
236+
237+
This alternative proposed a a protocol method `def __fingerprint__(self)` to the following classes in TensorFlow that inherit from Trackable:
238+
- `def_function.Function`
239+
- `Module`
240+
- `Dataset`
241+
- `ConcreteFunction`
242+
- `Asset`
243+
- `Constant`
244+
- `Variable`
245+
246+
When creating the SavedModel proto, we’d collect the fingerprints of each of these instances in the model. And then combine each of them into a single hash them again to create a final fingerprint that uniquely identifies the part of the SavedModel proto they belong to.
247+
248+
The disadvantage of this approach is that it is primarily in Python and works with the objects as defined in Python. Working in C++ is preferable to working in Python.
249+
250+
#### Text Format Proto
251+
252+
One idea is have the fingerprint be in [text format](https://developers.google.com/protocol-buffers/docs/text-format-spec) so it
253+
can be human-readable. However, the disadvantage is that using text format prevents us from making changes to the fields in the
254+
fingerprint protobuf. Text format is not designed with the same forward- and backward-compatibility properties as the binary format, so
255+
we avoid using it with production infrastructure.
256+
257+
### Future Ideas
258+
259+
#### Changes to the CLI
260+
261+
The SavedModel CLI will display the fingerprint in its output. This will be useful for clients to troubleshoot any problems with fingerprinting such that they do not need to load the SavedModel protobuf in Python and instead use only the commandline.
262+
263+
#### UUID
264+
265+
A UUID is not a good idea because it produces non-deterministic artifacts which are major resource wasters for build systems.
266+
267+
To prevent this from happening, we could have a flag to disable fingerprinting (manually or automatically if we detect we are using Bazel) that could be activated by clients who need to have deterministic artifacts. However, we are wary of introducing another knob to an already complex system.
268+
269+
## Appendix
270+
271+
### Concerns (to be verified by experiments)
272+
273+
- Roundtrip-ability: is the fingerprint same before and after loading?
274+
- A fingerprint for the same program is not guaranteed to be the same depending on the type of fingerprint.
275+
- How much overhead does it add to serialization?
276+
- Since we are hashing the protobuf, we need to serialize it to a string twice. First, to calculate the hash. And second, to write the protobuf (with the hash field populated) to disk.
277+
- Additionally, how long will the hashing algorithm take?
278+
- Any security concerns?
279+
280+
### Dealing with Serialization Non-Determinism
281+
Non-determinism of the SavedModel protobuf makes fingerprinting tricky. This is why the fingerprint has a non-regularized file checksum and several regularized hashes, with the latter attempting to guarantee determinism. The additional work needed to remove sources of non-determinism from the protobuf makes the implementation of fingerprinting a complex project.
282+
283+
#### What changes between binaries in the graphdef?
284+
285+
This is not an exhaustive list:
286+
287+
- The variable names are suffixed with an ever-increasing integer since there is a UID in `tensorflow/python/framework/ops.py`, which starts at 0 and is incremented every time it is called from Python or C++.
288+
- Function names are also non-deterministic and are suffixed with a UID as well.
289+
- The order of functions in the function library is non-deterministic as well, since protobufs do not guarantee the order of maps.
290+
291+
#### Steps to deal with the non-determinism
292+
- Remove the UID from names
293+
- Add an auxiliary name to variable names
294+
- Do a “compiler pass” to recognize the auxiliary variable names and replace them with known names-> variable name regularization
295+
296+
297+
## Questions and Discussion Topics
298+
299+
(To be added)
300+
Loading
Loading

0 commit comments

Comments
 (0)