feat(compression): add DECODE operator insertion#3624
Conversation
Insert DECODE operators before consumers of compressed tensors. Each consumer gets its own DECODE operator to support alternate decompression memory, which resets allocations between DECODE invocations. After insertion, compressed tensors are rewritten to hold encoded data as UINT8 with shape matching byte count. BUG=part of tensorflow#3256
| if not consumers: | ||
| # Check if tensor is a subgraph output | ||
| is_output = tensor in subgraph.outputs | ||
| if is_output: |
There was a problem hiding this comment.
I guess if it_output should be outside of if not consumers? It might be consumed and an oiutput of subgraph?
There was a problem hiding this comment.
You're right: the check missed tensors that are both consumed by an operator and listed as a subgraph output, so a subgraph would have output the encoded bytes instead of the decoded values. Addressed in 3999889 by treating the output list as one more consumer, fed by a DECODE appended after the last operator.
f3a8e56 pulls forward a fix from later in the series: separate DECODEs per tensor don't work with alternate decompression memory when the decoded values are needed at the same time, and with output handling added, it's even more worth getting this right initially than rewriting it later. It's a separate commit for easy review, relying on the squash when the PR merges.
A compressed tensor can be listed in a subgraph's output list, where it is read not by an operator, but by the operator calling the subgraph (IF, WHILE), which copies subgraph outputs when the subgraph returns, or by the client, which reads model outputs after invocation. DECODE insertion previously checked the output list only for tensors with no consumers, and refused those as unsupported. A tensor both consumed and listed as an output slipped through: the pass rewired its consumers to decoded values, then rewrote the tensor to hold encoded bytes, which the output list delivered as if decoded. Treat the output list as one more consumer, one which reads its tensors only after the last operator runs. Append a DECODE after the last operator for each compressed tensor in the output list, and rewire the list entry to the decoded value. BUG=part of tensorflow#3256
A consumer reading several compressed tensors needs all their decoded values at once, but under alternate decompression memory, values produced by different DECODE operators cannot coexist: each DECODE resets the allocation offset during Prepare, placing every DECODE's outputs at the same address, so each DECODE overwrites the outputs of the one before it. Decoding a consumer's tensors with separate DECODE operators corrupts all but the last value. Decode all compressed tensors read by one consumer with a single DECODE operator carrying one encoded/ancillary input pair and one output per tensor. Outputs of a single DECODE coexist, since the allocation reset happens between operators, not between the outputs of one. The subgraph output list, treated as one more consumer, gets the same: one DECODE, appended after the last operator, decodes every compressed tensor in the list. BUG=part of tensorflow#3256
DECODE insertion sorted consumers and located insertion points with list.index, a linear scan of the operator list per lookup. Build a map of operator positions once per subgraph and consult it instead. The positions recorded before any insertion remain correct throughout: consumers are handled in reverse position order, so each insertion falls after every consumer still to be processed. BUG=part of tensorflow#3256
There was a problem hiding this comment.
Looks really good, almost there. What is here will create a valid and usable model, but not exactly as it should be.
Maximal reduction of the model is not yet achieved due to possible duplication of the ancillary tensor Buffer. Here is the scenario: Two const-tensors, either in the same or in different subgraphs can contain the exact same data. The TfLite Converter can (and does) create two tensors in the model, but using a single shared buffer. For DECODE insertion, the existing code works just fine for encoded tensors, as they are not given a new buffer, just new buffer content. But this means that the related ancillary tensors, can also share a single buffer. However, the current insertion code always creates a new Buffer object when creating ancillary tensors.
The output tensors of DECODE must be exact copies of the original unencoded tensors, meaning when the output tensors are created they must use the original tensor's TensorT object. Only the name of the output tensors can be modified (as is currently done by the insertion code), and the Buffer object set to None (buffer in the TensorT object is zero).
For the unit tests, maybe a comment could be added stating the models are just for unit test purposes and do not represent fully valid models. Same for the encoded_data and ancillary_data of the CompressionResult objects. Something saying these are just dummy payloads for unit testing and do not correspond to the model's tensor data. Someone looking at these for the first time might mistake these for valid examples and be confused.
| sg = model.subgraphs[0] | ||
| weights = sg.tensor_by_name("shared_weights") | ||
|
|
||
| consumers = decode_insert._find_tensor_consumers(sg, weights) |
There was a problem hiding this comment.
Use of a private method (_find_tensor_consumers). Maybe have a set of public methods just for use by unit tests?
| model = model_editor.Model(subgraphs=[ | ||
| model_editor.Subgraph( | ||
| tensors=[weights], | ||
| operators=[ | ||
| model_editor.Operator( | ||
| opcode=tflite.BuiltinOperator.FULLY_CONNECTED, | ||
| inputs=[input_t, weights], | ||
| outputs=[output_t], | ||
| ) | ||
| ], | ||
| ) |
There was a problem hiding this comment.
Doesn't the Subgraph inputs/outputs need to be specified? Otherwise the subgraph compiler will just assign empty vectors to the tflite.SubGraphT.
| model = model_editor.Model(subgraphs=[ | ||
| model_editor.Subgraph( | ||
| tensors=[table], | ||
| operators=[ | ||
| model_editor.Operator( | ||
| opcode=tflite.BuiltinOperator.FULLY_CONNECTED, | ||
| inputs=[input_t, weights], | ||
| outputs=[output_t], | ||
| ) | ||
| ], | ||
| outputs=[output_t, table], | ||
| ) | ||
| ]) |
There was a problem hiding this comment.
Doesn't the Subgraph inputs need to be specified? Otherwise the subgraph compiler will just assign an empty vector in the tflite.SubGraphT.
| encoded_data=b'\x00\x00', | ||
| ancillary_data=_make_dummy_ancillary_data(), |
There was a problem hiding this comment.
mismatch between encoded_data and the ancillary_data. Perhaps _make_dummy_ancillary_data should have arguments so that it can generate a DCM that matches the model data? And encoded_data appears to be too short in length.
| def test_insert_single_decode_operator(self): | ||
| """DECODE operator inserted before FC that uses compressed weights.""" | ||
| model = _build_simple_fc_model() | ||
| weights_tensor = model.subgraphs[0].tensor_by_name("weights") |
There was a problem hiding this comment.
unused variable weights_tensor
| model = model_editor.Model(subgraphs=[ | ||
| model_editor.Subgraph( | ||
| tensors=[weights], | ||
| operators=[ | ||
| model_editor.Operator( | ||
| opcode=tflite.BuiltinOperator.FULLY_CONNECTED, | ||
| inputs=[input1, weights], | ||
| outputs=[output1], | ||
| ), | ||
| model_editor.Operator( | ||
| opcode=tflite.BuiltinOperator.FULLY_CONNECTED, | ||
| inputs=[input2, weights], | ||
| outputs=[output2], | ||
| ), | ||
| ], | ||
| ) |
There was a problem hiding this comment.
Doesn't the Subgraph inputs/outputs need to be specified? Otherwise the subgraph compiler will just assign empty vectors to the tflite.SubGraphT.
| # The two DECODEs should share the same ancillary tensor | ||
| self.assertIs(decode_op1.inputs[1], decode_op2.inputs[1]) |
There was a problem hiding this comment.
also check the two DECODES share the same encoded tensor
| self.assertIs(consumer_decode.inputs[1], output_decode.inputs[1]) | ||
|
|
There was a problem hiding this comment.
also check both DECODEs share the encoded tensor
| opcode=tflite.BuiltinOperator.FULLY_CONNECTED, | ||
| inputs=[input_t, weights1, weights2], |
There was a problem hiding this comment.
CONCATENATION would be a better example here (with output shape [6,4])
|
|
||
| # Verify DCM header | ||
| dcm_bytes = ancillary_tensor.array[:16] | ||
| self.assertEqual(dcm_bytes[0], 0) # decode_type = LUT |
Insert DECODE operators before consumers of compressed tensors. Each
consumer gets its own DECODE operator to support alternate decompression
memory, which resets allocations between DECODE invocations.
After insertion, compressed tensors are rewritten to hold encoded data
as UINT8 with shape matching byte count.
Compression tooling only; adds decode_insert.py and its test, no
runtime changes.
Part of the DECODE compression-tooling stack.
BUG=#3256