Skip to content

feat(compression): add DECODE operator insertion#3624

Open
rkuester wants to merge 4 commits into
tensorflow:mainfrom
rkuester:feat-decode-submit
Open

feat(compression): add DECODE operator insertion#3624
rkuester wants to merge 4 commits into
tensorflow:mainfrom
rkuester:feat-decode-submit

Conversation

@rkuester

@rkuester rkuester commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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

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
@rkuester
rkuester requested a review from a team as a code owner July 6, 2026 20:42
@rkuester rkuester added the ci:full Triggers the comprehensive cross-platform test suite. label Jul 6, 2026
@rkuester
rkuester temporarily deployed to integration-test July 6, 2026 20:42 — with GitHub Actions Inactive
@rkuester
rkuester requested a review from veblush July 6, 2026 22:34
if not consumers:
# Check if tensor is a subgraph output
is_output = tensor in subgraph.outputs
if is_output:

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.

I guess if it_output should be outside of if not consumers? It might be consumed and an oiutput of subgraph?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@ddavis-2015 ddavis-2015 Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@rkuester @veblush Re: Passing the output of DECODE to or from a subgraph

The design document will be updated to address what to do in this situation.

Comment thread tensorflow/lite/micro/compression/decode_insert.py Outdated
rkuester added 3 commits July 7, 2026 12:50
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
@rkuester
rkuester temporarily deployed to integration-test July 7, 2026 18:48 — with GitHub Actions Inactive
@rkuester
rkuester requested review from ddavis-2015 and veblush July 7, 2026 19:04

@ddavis-2015 ddavis-2015 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use of a private method (_find_tensor_consumers). Maybe have a set of public methods just for use by unit tests?

Comment on lines +53 to +63
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],
)
],
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't the Subgraph inputs/outputs need to be specified? Otherwise the subgraph compiler will just assign empty vectors to the tflite.SubGraphT.

Comment on lines +145 to +157
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],
)
])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't the Subgraph inputs need to be specified? Otherwise the subgraph compiler will just assign an empty vector in the tflite.SubGraphT.

Comment on lines +183 to +184
encoded_data=b'\x00\x00',
ancillary_data=_make_dummy_ancillary_data(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

unused variable weights_tensor

Comment on lines +98 to +113
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],
),
],
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't the Subgraph inputs/outputs need to be specified? Otherwise the subgraph compiler will just assign empty vectors to the tflite.SubGraphT.

Comment on lines +310 to +311
# The two DECODEs should share the same ancillary tensor
self.assertIs(decode_op1.inputs[1], decode_op2.inputs[1])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also check the two DECODES share the same encoded tensor

Comment on lines +378 to +379
self.assertIs(consumer_decode.inputs[1], output_decode.inputs[1])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also check both DECODEs share the encoded tensor

Comment on lines +412 to +413
opcode=tflite.BuiltinOperator.FULLY_CONNECTED,
inputs=[input_t, weights1, weights2],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

use DecodeType.LUT

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

Labels

ci:full Triggers the comprehensive cross-platform test suite.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants