mirror of
https://github.com/vee1e/tflite-micro.git
synced 2026-09-02 02:07:27 +00:00
* feat(compression): add DECODE operator insertion 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 #3256 * fix(compression): decode compressed subgraph outputs 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 #3256 * fix(compression): batch multiple compressed tensors per DECODE 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 treatment. One DECODE, appended after the last operator, decodes every compressed tensor in the list. BUG=part of #3256 * refactor(compression): precompute operator positions 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, because consumers are handled in reverse position order, so each insertion falls after every consumer still to be processed. BUG=part of #3256 * test(compression): add runtime tests for DECODE across subgraphs Add a test suite that exercises DECODE outputs crossing subgraph boundaries on the TFLM interpreter, rather than only checking the rewritten flatbuffer structure. The tests build multi-subgraph WHILE models with model_editor, compress constants with the LUT compressor, insert DECODE operators with decode_insert, and verify inference results, in both arena and alternate decompression memory modes. The case of a DECODE output feeding a WHILE input, with a second DECODE in the cond subgraph and alternate decompression memory in use, requires the WhileEval fix from #3633 (issue #3632). WHILE formerly re-read its inputs after invoking the cond subgraph, picking up the value the cond subgraph's DECODE wrote over shared alternate memory. * feat(compression): add tensor copying and equality to model_editor Add Tensor.copy(), which duplicates a tensor's backing TensorT and shares the original's Buffer object. Duplicating the TensorT preserves fields model_editor does not otherwise manage, such as is_variable and shape_signature. An optional name argument gives the copy its own name. Clients that need a data-less copy, such as tooling that creates stand-in tensors, can assign None to the copy's buffer. Add Tensor.equal(), a field-wise equality over the backing TensorT, quantization, and buffer. Fields unknown to model_editor participate via recursive comparison, so clients can compare tensors without enumerating fields. Buffers compare by identity, mirroring how the model expresses buffer sharing. * feat(compression): add buffer deduplication to model_editor Add dedupe_buffers(), which repoints tensors whose buffers hold byte-identical contents at one canonical Buffer object, mirroring the TfLite converter's deduplication of identical constants. Tensors marked is_variable are left alone, since mutable data must not alias. The walk covers every tensor the compiler collects, including tensors inline on operators that never appear in a subgraph's tensor list. Merged-away buffers linger in model.buffers until pruned. * feat(compression): add buffer pruning to model_editor Add prune_buffers(), which rebuilds model.buffers with only the conventional empty buffer 0 and the buffers some tensor references, renumbering indices in the process. Models built from scratch keep an empty buffer list and compile only referenced buffers, so pruning matters for models from read(), whose buffer list the compiler preserves wholesale, including entries orphaned by editing. * fix(compression): make DECODE outputs full copies of their originals Create the output tensor of a DECODE operator by copying the original tensor, clearing its data, and renaming it, rather than by building a new tensor from the original's shape, dtype, and quantization. Copying preserves TensorT fields the insertion code does not otherwise handle, such as is_variable and shape_signature, so the decoded stand-in is indistinguishable from the original tensor it replaces. Verify the output against a copy of the original snapshotted before insertion rewrites it, compared with field-wise tensor equality so every field participates without the test enumerating them. * fix(compression): share buffers among aliases by deduplication Distinct tensors can share one buffer, in the same or different subgraphs, where the converter deduplicated identical constants. Give each rewritten encoded tensor and each ancillary tensor a fresh buffer, then merge byte-identical buffers and prune unreferenced ones after insertion. Sharing survives compression wherever aliases compress to identical results, extends to any ancillary data that coincides, and dissolves where results diverge (possible for tensors sharing bytes but quantized with different structures), rather than one alias corrupting another through a shared buffer rewritten in place. Skip, with a warning, compressed tensors that share a buffer with an uncompressed tensor. The uncompressed data must remain in the model for the other tensors, so compressing such an alias cannot reduce model size. * docs(compression): reword DECODE insertion docstring Describe the placement of DECODE operators in terms of the operator's contract. Outputs have a lifetime limited to the very next operator in the subgraph, and DECODE trades increased latency for decreased memory usage. Remove the explanation of interpreter alternate-memory behavior that previously justified the per-consumer placement, along with a confusing aside about clients reading model outputs. Describe the output tensor as a copy of the original, matching the implementation. * feat(compression): add tensor consumer lookup to model_editor Add Subgraph.consumers_of(), which returns the operators reading a given tensor, in subgraph order. Replace decode_insert's private helper _find_tensor_consumers with it. The helper's unit test called a private method of decode_insert; consumer lookup is now a public API, tested in model_editor_test. * test(compression): declare subgraph inputs and outputs in test models Specify the subgraph inputs and outputs in the three test model builders. Two builders previously declared neither, and the third declared only its outputs, so the subgraph compiler emitted empty vectors for whatever was missing, making the models structurally unlike anything the converter produces. * test(compression): remove unused variables Remove two weights_tensor assignments never read by their tests. * test(compression): assert DECODEs share the encoded tensor When one compressed tensor feeds multiple DECODE operators, assert that the operators read the same encoded tensor object, alongside the existing assertion that they share the ancillary tensor. Cover both situations in which one tensor feeds multiple DECODE operators, a tensor with two consumers and a tensor both consumed and listed as a subgraph output. * test(compression): compare decode type against DecodeType.LUT Compare the DCM's decode type byte against the DecodeType.LUT constant instead of a magic zero. Convert the DCM slice from a numpy array to bytes first, so that indexing yields a plain integer whose comparison defers to the constant's own equality. * test(compression): use CONCATENATION in multi-input DECODE test Exercise the one-DECODE-per-consumer batching with a CONCATENATION of two compressed tensors instead of a FULLY_CONNECTED with an extra weights input, which is not a valid FC signature. CONCATENATION takes any number of inputs, so the model resembles something a converter could produce. * test(compression): make dummy compression payloads self-consistent Replace the fixed dummy ancillary data helper with one that builds a whole CompressionResult, parameterized by element count, index bitwidth, and value table, so each test passes values consistent with the tensor it compresses and the encoded data is sized accordingly. State in the module docstring that the test models and payloads are structural fixtures, not valid runnable models or decodable data, so a reader does not mistake them for real examples. * test(compression): cover buffer alias divergence and partial coverage Two tensors can share one buffer when the converter deduplicates identical constants. Add tests for the two situations in which insertion cannot preserve that sharing. In the first, both tensors are compressed but their compression results differ. Insertion dissolves the sharing, and the test verifies that each tensor receives its own encoded and ancillary buffers holding its own results. In the second, only one of the tensors is compressed. The uncompressed tensor keeps the original data in the model, so compressing its alias would grow the model rather than shrink it. Insertion declines to compress, and the test verifies that no DECODE operator is inserted, that the tensor is untouched, and that a warning explains why. * test(compression): exercise insertion on a model read from a flatbuffer All other insertion tests build their models from scratch, and a from-scratch model keeps an empty buffer list, which makes buffer pruning a no-op. Add a test that round-trips a model through build() and read() before insertion, then verifies the packed result carries the DECODE operator with its encoded and ancillary data, and that the buffer orphaned when compression rewrote the weights tensor is pruned rather than left in the model. * test(compression): share one flatbuffer packing helper Two test classes each defined an identical method that packs a model into a flatbuffer. Replace both with one module-level function. The next commit adds fixtures at module level, which will also reuse the function. * test(compression): assert absent inputs survive The flatbuffer schema marks an absent optional operator input with an index of -1. E.g., a fully-connected operator uses that index when it has no bias. In fact, a fused LSTM leaves most of its inputs absent. The model editor does not currently preserve an absent input. Add expected-to-fail tests to expose the bug. The next commit will fix the bug and drop the expected-failure flags. * fix(compression): preserve absent optional operator inputs The flatbuffer schema marks an absent optional operator input with an index of -1. Read that index as None among an operator's inputs, and write None back out as -1. Reject any other negative index. Drop the expected-failure flags from the tests added by the previous commit, and add two more tests covering iteration and the consumer lookup. * fix(compression): reject negative tensor indices Only an operator's inputs give a negative tensor index a meaning, an index of -1 marking an absent optional input. An operator's outputs and a subgraph's inputs and outputs give none. Reject a negative index in those three. Reading one used to substitute a tensor counted from the end of the subgraph's tensor list, and writing the model back out then recorded that tensor's real index. A malformed model became a well-formed one naming a different tensor. Resolving a subgraph's inputs and outputs no longer needs a guard against an absent or empty list, since resolving nothing yields the empty list a subgraph already starts with. * test(compression): strengthen the metadata pruning test Pruning a model's buffers must not disturb its metadata. The test for that was weak and had no buffers to remove. Fix by orphaning a buffer, so pruning removes it and shifts the buffers that remain. Assert the buffer count drops. Check that the metadata still survives a roundtrip. * style(compression): drop an unused import The model editor imports dataclasses.field and never uses it. * docs(compression): say where the appended DECODE goes The docstring said a DECODE is appended, without saying to what.
346 lines
13 KiB
Python
346 lines
13 KiB
Python
# Copyright 2026 The TensorFlow Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""DECODE operator insertion into TFLite model graphs.
|
|
|
|
This module inserts DECODE operators into a compressed model. DECODE operators
|
|
transform encoded tensors (with their paired ancillary data tensors) into
|
|
tensors ready for use by downstream operators.
|
|
|
|
The DECODE operator is registered as a custom operator named "TFLM_DECODE".
|
|
Each DECODE output requires two inputs: the encoded tensor and the ancillary
|
|
data tensor (containing the DCM header and decode-type-specific data).
|
|
"""
|
|
|
|
import warnings
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass
|
|
|
|
from tflite_micro.tensorflow.lite.micro.compression import compressor
|
|
from tflite_micro.tensorflow.lite.micro.compression import model_editor
|
|
from tflite_micro.tensorflow.lite.python import schema_py_generated as tflite
|
|
|
|
# Custom operator name for DECODE
|
|
DECODE_CUSTOM_OP_NAME = "TFLM_DECODE"
|
|
|
|
|
|
@dataclass
|
|
class _CompressedTensorInfo:
|
|
"""Information about a compressed tensor for DECODE insertion."""
|
|
subgraph_idx: int
|
|
tensor_idx: int
|
|
tensor: model_editor.Tensor
|
|
encoded_data: bytes
|
|
ancillary_data: bytes
|
|
consumers: list[model_editor.Operator]
|
|
is_output: bool
|
|
|
|
|
|
def _create_ancillary_tensor(
|
|
ancillary_data: bytes,
|
|
original_tensor: model_editor.Tensor,
|
|
) -> model_editor.Tensor:
|
|
"""Create an ancillary data tensor for a compressed tensor.
|
|
|
|
Args:
|
|
ancillary_data: The complete ancillary data (DCM + type-specific data).
|
|
original_tensor: The original tensor being decoded, for naming.
|
|
|
|
Returns:
|
|
A new Tensor containing the ancillary data.
|
|
"""
|
|
name = None
|
|
if original_tensor.name:
|
|
name = f"{original_tensor.name}_ancillary"
|
|
|
|
return model_editor.Tensor(
|
|
shape=(len(ancillary_data), ),
|
|
dtype=tflite.TensorType.UINT8,
|
|
data=ancillary_data,
|
|
name=name,
|
|
)
|
|
|
|
|
|
def _create_output_tensor(
|
|
original_tensor: model_editor.Tensor, ) -> model_editor.Tensor:
|
|
"""Create the output tensor for a DECODE operator.
|
|
|
|
The output tensor is a copy of the original tensor, differing only in
|
|
name and in having no data: the DECODE operator produces the values
|
|
at runtime.
|
|
|
|
Args:
|
|
original_tensor: The original tensor being decoded.
|
|
|
|
Returns:
|
|
A new Tensor for the DECODE output.
|
|
"""
|
|
name = None
|
|
if original_tensor.name:
|
|
name = f"{original_tensor.name}_decoded"
|
|
|
|
tensor = original_tensor.copy(name=name)
|
|
tensor.buffer = None
|
|
return tensor
|
|
|
|
|
|
def _rewire_consumers(
|
|
consumers: list[model_editor.Operator],
|
|
old_tensor: model_editor.Tensor,
|
|
new_tensor: model_editor.Tensor,
|
|
) -> None:
|
|
"""Replace old_tensor with new_tensor in all consumer inputs."""
|
|
for consumer in consumers:
|
|
consumer.inputs = [
|
|
new_tensor if t is old_tensor else t for t in consumer.inputs
|
|
]
|
|
|
|
|
|
def _rewrite_encoded_tensor(
|
|
tensor: model_editor.Tensor,
|
|
encoded_data: bytes,
|
|
) -> None:
|
|
"""Rewrite a compressed tensor to hold encoded data.
|
|
|
|
The original tensor contained uncompressed values with quantization. After
|
|
compression, it holds packed indices (or other encoded form) as raw bytes.
|
|
The tensor receives a fresh Buffer, leaving the original buffer and any
|
|
tensors aliasing it untouched; identical encodings converge again in the
|
|
final deduplication pass.
|
|
|
|
Args:
|
|
tensor: The tensor to rewrite.
|
|
encoded_data: The compressed/encoded data bytes.
|
|
"""
|
|
tensor.shape = (len(encoded_data), )
|
|
tensor.dtype = tflite.TensorType.UINT8
|
|
tensor.quantization = None
|
|
tensor.buffer = model_editor.Buffer(data=encoded_data)
|
|
|
|
|
|
def _drop_partially_covered_buffers(
|
|
model: model_editor.Model,
|
|
compression_results: dict[tuple[int, int], compressor.CompressionResult],
|
|
) -> dict[tuple[int, int], compressor.CompressionResult]:
|
|
"""Drop compressed tensors whose buffer an uncompressed tensor shares.
|
|
|
|
The uncompressed tensor keeps the original data in the model, so
|
|
compressing any alias of its buffer adds encoded data, ancillary
|
|
data, and DECODE latency without reducing model size. Warn and
|
|
return the results without the dropped entries.
|
|
|
|
Args:
|
|
model: The model the results apply to.
|
|
compression_results: Map from (subgraph_idx, tensor_idx) to
|
|
CompressionResult.
|
|
|
|
Returns:
|
|
compression_results, minus entries for partially covered buffers.
|
|
"""
|
|
coordinates = {
|
|
id(model.subgraphs[s].tensors[t]): (s, t)
|
|
for (s, t) in compression_results
|
|
}
|
|
by_buffer: dict[int, list[model_editor.Tensor]] = defaultdict(list)
|
|
for tensor in model_editor.iter_tensors(model):
|
|
if tensor.buffer is not None:
|
|
by_buffer[id(tensor.buffer)].append(tensor)
|
|
|
|
results = dict(compression_results)
|
|
for aliases in by_buffer.values():
|
|
covered = [t for t in aliases if id(t) in coordinates]
|
|
if covered and len(covered) < len(aliases):
|
|
uncovered = [t for t in aliases if id(t) not in coordinates]
|
|
warnings.warn(
|
|
f"Not compressing tensor(s) "
|
|
f"{[t.name for t in covered]}: sharing a buffer with "
|
|
f"uncompressed tensor(s) {[t.name for t in uncovered]}, whose "
|
|
"data stays in the model, so compression cannot reduce model "
|
|
"size.",
|
|
stacklevel=3)
|
|
for tensor in covered:
|
|
del results[coordinates[id(tensor)]]
|
|
return results
|
|
|
|
|
|
def insert_decode_operators(
|
|
model: model_editor.Model,
|
|
compression_results: dict[tuple[int, int], compressor.CompressionResult],
|
|
) -> None:
|
|
"""Insert DECODE operators for all compressed tensors.
|
|
|
|
This function modifies the model in-place, inserting a DECODE operator
|
|
before any operator that uses a compressed tensor as input. For
|
|
compressed tensors listed as subgraph outputs, it appends a DECODE to
|
|
the end of the subgraph.
|
|
|
|
A separate DECODE is inserted before each consumer, and one DECODE
|
|
decodes all the compressed tensors its consumer reads. DECODE outputs
|
|
are tensors with a lifetime limited to the very next operator in the
|
|
subgraph, so sharing one DECODE among multiple consumers would
|
|
violate the lifetime rule. The DECODE operator trades increased
|
|
latency for decreased memory usage.
|
|
|
|
For each consumer of compressed tensors:
|
|
1. Create an ancillary data tensor (DCM + type-specific data) for each
|
|
compressed tensor the consumer reads
|
|
2. Create an output tensor as a copy of each original tensor
|
|
3. Insert one DECODE operator immediately before the consumer
|
|
4. Rewire the consumer to use the DECODE outputs
|
|
|
|
A subgraph's output list is treated as one more consumer, one which
|
|
reads its tensors only after the last operator runs: a calling
|
|
operator (IF, WHILE) copies subgraph outputs when the subgraph
|
|
returns. Compressed tensors in the output list are therefore decoded
|
|
by a single DECODE appended after the last operator, and their output
|
|
list entries are rewired to the decoded values.
|
|
|
|
Distinct tensors can share one buffer, in the same or different
|
|
subgraphs, where the converter deduplicated identical constants.
|
|
Compressed tensors sharing a buffer with uncompressed tensors are
|
|
skipped with a warning: the uncompressed data must stay in the model,
|
|
so compressing an alias cannot reduce model size. Otherwise each
|
|
rewritten tensor and ancillary tensor receives its own buffer, and a
|
|
final deduplication pass merges byte-identical buffers and prunes
|
|
unreferenced ones, preserving the converter's sharing wherever
|
|
compression results allow and dissolving it where they diverge.
|
|
|
|
Args:
|
|
model: The model to modify in-place.
|
|
compression_results: Map from (subgraph_idx, tensor_idx) to the
|
|
CompressionResult containing ancillary_data.
|
|
"""
|
|
compression_results = _drop_partially_covered_buffers(
|
|
model, compression_results)
|
|
|
|
# Group compressed tensors by subgraph
|
|
by_subgraph: dict[int, list[_CompressedTensorInfo]] = defaultdict(list)
|
|
|
|
for (sg_idx, tensor_idx), result in compression_results.items():
|
|
subgraph = model.subgraphs[sg_idx]
|
|
tensor = subgraph.tensors[tensor_idx]
|
|
consumers = subgraph.consumers_of(tensor)
|
|
is_output = tensor in subgraph.outputs
|
|
|
|
if not consumers and not is_output:
|
|
warnings.warn(
|
|
f"Compressed tensor {tensor.name!r} (subgraph {sg_idx}, "
|
|
f"tensor {tensor_idx}) has no consumers and is not a subgraph "
|
|
"output. No DECODE operator will be inserted.",
|
|
stacklevel=2)
|
|
continue
|
|
|
|
info = _CompressedTensorInfo(
|
|
subgraph_idx=sg_idx,
|
|
tensor_idx=tensor_idx,
|
|
tensor=tensor,
|
|
encoded_data=result.encoded_data,
|
|
ancillary_data=result.ancillary_data,
|
|
consumers=consumers,
|
|
is_output=is_output,
|
|
)
|
|
by_subgraph[sg_idx].append(info)
|
|
|
|
# Process each subgraph
|
|
for sg_idx, tensor_infos in by_subgraph.items():
|
|
subgraph = model.subgraphs[sg_idx]
|
|
|
|
# Cache ancillary tensors by content to avoid duplicates within
|
|
# this subgraph. Each DECODE needs its own output tensor, but
|
|
# DECODEs whose ancillary data coincides can read one tensor.
|
|
ancillary_cache: dict[bytes, model_editor.Tensor] = {}
|
|
|
|
# Track tensors to rewrite after all output tensors are created, since
|
|
# _create_output_tensor reads the original tensor's shape/dtype/quantization.
|
|
tensors_to_rewrite: dict[model_editor.Tensor, bytes] = {}
|
|
|
|
def ancillary_for(info: _CompressedTensorInfo) -> model_editor.Tensor:
|
|
"""Reuse or create the ancillary tensor for info's ancillary data."""
|
|
ancillary = ancillary_cache.get(info.ancillary_data)
|
|
if ancillary is None:
|
|
ancillary = _create_ancillary_tensor(info.ancillary_data, info.tensor)
|
|
subgraph.tensors.append(ancillary)
|
|
ancillary_cache[info.ancillary_data] = ancillary
|
|
return ancillary
|
|
|
|
def build_decode(
|
|
infos: list[_CompressedTensorInfo]
|
|
) -> tuple[model_editor.Operator, list[model_editor.Tensor]]:
|
|
"""Build one DECODE operator decoding all of infos' tensors.
|
|
|
|
Returns the operator and its decoded output tensors, parallel to
|
|
infos.
|
|
"""
|
|
inputs = []
|
|
outputs = []
|
|
for info in infos:
|
|
ancillary_tensor = ancillary_for(info)
|
|
tensors_to_rewrite[info.tensor] = info.encoded_data
|
|
decoded = _create_output_tensor(info.tensor)
|
|
subgraph.tensors.append(decoded)
|
|
inputs.extend([info.tensor, ancillary_tensor])
|
|
outputs.append(decoded)
|
|
op = model_editor.Operator(
|
|
opcode=tflite.BuiltinOperator.CUSTOM,
|
|
custom_code=DECODE_CUSTOM_OP_NAME,
|
|
inputs=inputs,
|
|
outputs=outputs,
|
|
)
|
|
return op, outputs
|
|
|
|
# Positions of the original operators, computed once so the sort and
|
|
# insertions below avoid a linear scan per lookup.
|
|
op_position = {op: i for i, op in enumerate(subgraph.operators)}
|
|
|
|
# Group compressed tensors by consumer, then handle consumers in
|
|
# reverse position order so insertions don't invalidate positions:
|
|
# each insertion falls after every consumer still to be processed,
|
|
# leaving the recorded positions valid.
|
|
by_consumer: dict[model_editor.Operator, list[_CompressedTensorInfo]] = {}
|
|
for info in tensor_infos:
|
|
for consumer in info.consumers:
|
|
by_consumer.setdefault(consumer, []).append(info)
|
|
|
|
for consumer in sorted(by_consumer,
|
|
key=lambda op: op_position[op],
|
|
reverse=True):
|
|
infos = by_consumer[consumer]
|
|
decode_op, decoded_tensors = build_decode(infos)
|
|
|
|
# Insert DECODE immediately before this consumer
|
|
subgraph.operators.insert(op_position[consumer], decode_op)
|
|
|
|
# Rewire only this consumer to use the decoded outputs
|
|
for info, decoded in zip(infos, decoded_tensors):
|
|
_rewire_consumers([consumer], info.tensor, decoded)
|
|
|
|
# Decode compressed tensors read from the subgraph's output list, all
|
|
# with one DECODE appended after the last operator (see docstring).
|
|
output_infos = [info for info in tensor_infos if info.is_output]
|
|
if output_infos:
|
|
decode_op, decoded_tensors = build_decode(output_infos)
|
|
subgraph.operators.append(decode_op)
|
|
for info, decoded in zip(output_infos, decoded_tensors):
|
|
subgraph.outputs = [
|
|
decoded if t is info.tensor else t for t in subgraph.outputs
|
|
]
|
|
|
|
# Rewrite encoded tensors after all output tensors are created
|
|
for tensor, encoded_data in tensors_to_rewrite.items():
|
|
_rewrite_encoded_tensor(tensor, encoded_data)
|
|
|
|
# Every rewrite and ancillary tensor made a fresh buffer; converge
|
|
# byte-identical ones and drop those left unreferenced, preserving
|
|
# the sharing the converter created wherever results allow.
|
|
model_editor.dedupe_buffers(model)
|
|
model_editor.prune_buffers(model)
|