blob: 5db47aa3422717d47e5ed515b2930465a19bc44e [file] [edit]
/*
* Copyright 2024 The ChromiumOS Authors
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <algorithm>
#include <fstream>
#include <iostream>
#include <string>
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/flags/usage.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "tensorflow/lite/core/model_builder.h"
#include "tensorflow/lite/schema/schema_utils.h"
ABSL_FLAG(std::string, input, "", "input tflite model");
ABSL_FLAG(std::string, output, "", "output tflite model");
ABSL_FLAG(std::vector<std::string>,
op_index,
std::vector<std::string>{},
"operator indices");
namespace {
constexpr absl::string_view kUsage = R"(
decompose_model <list|decompose> [options]
Available subcommands:
list --input=<input tflite file>
Lists all operators in the |input| and shows the their information.
decompose --input=<input tflite file> --output=<output tflite file>
--op_index=<operator indices>
Decomposes the |input| model by extracting the operators with indices
|op_index| and rebuild a model with the extracted operators, where |op_index|
is the list of indices seperated by comma, e.g. `--op_index=0,2,3`.
The new model is saved to |output|.
)";
void PrintUsage() {
std::cerr << kUsage << std::endl;
}
std::string TableStringify(const std::vector<std::vector<std::string>>& rows) {
if (rows.empty()) {
return "";
}
std::vector<int> widths(rows[0].size());
for (auto& row : rows) {
for (int i = 0; i < row.size(); ++i) {
widths[i] = std::max(widths[i], static_cast<int>(row[i].size()));
}
}
std::string message;
for (auto& row : rows) {
std::vector<std::string> items;
for (int i = 0; i < row.size(); ++i) {
items.push_back(absl::StrFormat("%-*s", widths[i], row[i]));
}
absl::StrAppendFormat(&message, "%s\n", absl::StrJoin(items, " "));
}
return message;
}
std::unique_ptr<tflite::ModelT> GetUnpackedModel(const std::string& path) {
std::unique_ptr<tflite::FlatBufferModel> fb_model =
tflite::FlatBufferModel::BuildFromFile(path.c_str());
if (!fb_model) {
std::cerr << "Failed to build model from file." << std::endl;
return nullptr;
}
std::unique_ptr<tflite::ModelT> model(fb_model->GetModel()->UnPack());
if (!model) {
std::cerr << "Failed to unpack model." << std::endl;
return nullptr;
}
return model;
}
// Decomposes |model|. Extracts the operators of indices |op_indices| as a new
// model and re-pack it into |model|.
bool DecomposeModel(tflite::ModelT& model, const std::vector<int>& op_indices) {
if (model.subgraphs.size() != 1) {
return false;
}
auto& subgraph = model.subgraphs[0];
// Prunes the other operators and keeps only operators with indices
// |op_indices|.
auto& operators = subgraph->operators;
for (int i = 0; i < op_indices.size(); ++i) {
if (operators.size() <= op_indices[i]) {
return false;
}
operators[i] = std::move(operators[op_indices[i]]);
}
operators.resize(op_indices.size());
// Prunes the unused tensors.
std::map<int, int> tensor_map;
int tensor_map_index = 0;
for (auto& op : operators) {
for (auto& tensors : {std::ref(op->inputs), std::ref(op->outputs)}) {
for (int& index : tensors.get()) {
// Optional input -1 should be skipped.
if (index < 0) {
continue;
}
auto it = tensor_map.find(index);
if (it == tensor_map.end()) {
tensor_map.emplace(index, tensor_map_index++);
}
index = tensor_map[index];
}
}
}
std::vector<std::unique_ptr<tflite::TensorT>> new_tensors(tensor_map_index);
for (auto [k, v] : tensor_map) {
new_tensors[v] = std::move(subgraph->tensors[k]);
}
subgraph->tensors = std::move(new_tensors);
// Prunes the unused buffers.
// TODO: b/331900692 - Randomize the buffer data.
// 0th entry of the buffer array should be an empty buffer for tensors
// without associated buffers.
std::map<int, int> buffer_map = {{0, 0}};
int buffer_map_index = 1;
for (const auto& tensor : subgraph->tensors) {
int buffer_idx = tensor->buffer;
auto it = buffer_map.find(buffer_idx);
if (it == buffer_map.end()) {
buffer_map.emplace(buffer_idx, buffer_map_index++);
}
tensor->buffer = buffer_map[buffer_idx];
}
std::vector<std::unique_ptr<tflite::BufferT>> new_buffers(buffer_map_index);
for (auto [k, v] : buffer_map) {
new_buffers[v] = std::move(model.buffers[k]);
}
model.buffers = std::move(new_buffers);
// TODO: b/331900692 - Consider also pruning unused operator code.
// Reassigns input/output of the remaining operators with empty buffers to
// input/output of the subgraph.
subgraph->inputs.clear();
subgraph->outputs.clear();
std::set<int> op_inputs, op_outputs;
for (auto& op : operators) {
for (int index : op->inputs) {
if (index < 0) {
continue;
}
int buffer_idx = subgraph->tensors[index]->buffer;
// Input of the subgraph should contain only empty buffers.
if (model.buffers[buffer_idx]->data.size() == 0) {
op_inputs.insert(index);
}
}
for (int index : op->outputs) {
op_outputs.insert(index);
}
}
// Any tensor as both input and output of operators is not considered as the
// input/output of the subgraph.
std::vector<int> inputs, outputs;
std::ranges::set_difference(op_inputs, op_outputs,
std::back_inserter(subgraph->inputs));
std::ranges::set_difference(op_outputs, op_inputs,
std::back_inserter(subgraph->outputs));
return true;
}
bool Decompose(const std::string& input_path,
const std::string& output_path,
const std::vector<int>& op_indices) {
std::unique_ptr<tflite::ModelT> model = GetUnpackedModel(input_path);
if (!model) {
return false;
}
if (!DecomposeModel(*model, op_indices)) {
std::cerr << "Failed to decompose model." << std::endl;
return false;
}
// Packs the model back.
flatbuffers::FlatBufferBuilder fbb;
const flatbuffers::Offset<tflite::Model> model_buffer =
tflite::Model::Pack(fbb, model.get());
FinishModelBuffer(fbb, model_buffer);
std::ofstream out(output_path);
out.write(reinterpret_cast<char*>(fbb.GetBufferPointer()), fbb.GetSize());
return true;
}
std::string DumpTensorsInfo(
const std::vector<int>& tensor_indices,
const std::vector<std::unique_ptr<tflite::TensorT>>& tensors) {
return absl::StrJoin(
tensor_indices, " ", [&tensors](std::string* out, int index) {
if (index < 0) {
absl::StrAppend(out, "#" + std::to_string(index));
return;
}
auto& tensor = tensors[index];
std::string message =
absl::StrFormat("%s[%s]#%d", EnumNameTensorType(tensor->type),
absl::StrJoin(tensor->shape, ","), index);
absl::StrAppend(out, message);
});
}
bool List(const std::string& input_path) {
std::unique_ptr<tflite::ModelT> model = GetUnpackedModel(input_path);
if (!model) {
return false;
}
if (model->subgraphs.size() != 1) {
return false;
}
auto& subgraph = model->subgraphs[0];
auto& operators = subgraph->operators;
std::vector<std::vector<std::string>> table;
table.push_back({"index", "operator", "input: type[shape]#index",
"output: type[shape]#index"});
for (int i = 0; i < operators.size(); ++i) {
auto& op = operators[i];
auto& op_code = model->operator_codes[op->opcode_index];
tflite::BuiltinOperator builtin_code =
tflite::GetBuiltinCode(op_code.get());
std::string op_name = tflite::EnumNameBuiltinOperator(builtin_code);
std::string input_dump = DumpTensorsInfo(op->inputs, subgraph->tensors);
std::string output_dump = DumpTensorsInfo(op->outputs, subgraph->tensors);
table.push_back({std::to_string(i), op_name, input_dump, output_dump});
}
std::cout << "Subgraph inputs: "
<< DumpTensorsInfo(subgraph->inputs, subgraph->tensors)
<< std::endl;
std::cout << "Subgraph outputs: "
<< DumpTensorsInfo(subgraph->outputs, subgraph->tensors)
<< std::endl;
std::cout << TableStringify(table) << std::endl;
return true;
}
} // namespace
int main(int argc, char** argv) {
absl::SetProgramUsageMessage(kUsage);
std::vector<char*> args = absl::ParseCommandLine(argc, argv);
if (args.size() < 2) {
PrintUsage();
return 1;
}
std::string subcmd(args[1]);
if (subcmd == "decompose") {
std::string input_path = absl::GetFlag(FLAGS_input);
std::string output_path = absl::GetFlag(FLAGS_output);
std::vector<std::string> raw_op_indices = absl::GetFlag(FLAGS_op_index);
if (input_path.empty() || output_path.empty() || raw_op_indices.empty()) {
PrintUsage();
return 1;
}
std::vector<int> op_indices;
for (std::string& raw_index : raw_op_indices) {
int index = -1;
if (!absl::SimpleAtoi(raw_index, &index)) {
std::cerr << "Failed to convert index: " << raw_index << std::endl;
PrintUsage();
return 1;
}
op_indices.push_back(index);
}
std::sort(op_indices.begin(), op_indices.end());
auto it = std::unique(op_indices.begin(), op_indices.end());
op_indices.erase(it, op_indices.end());
if (!Decompose(input_path, output_path, op_indices)) {
return 1;
}
} else if (subcmd == "list") {
std::string input_path = absl::GetFlag(FLAGS_input);
if (input_path.empty()) {
PrintUsage();
return 1;
}
if (!List(input_path)) {
return 1;
}
} else {
PrintUsage();
return 1;
}
return 0;
}