Skip to content

Commit d1563b6

Browse files
salilsdesaifacebook-github-bot
authored andcommitted
Executorch Model Size Analysis Tool (#68)
Summary: Pull Request resolved: #68 Adds a Size Analysis Tool for Executorch models, which provides information about tensor data and delegate blobs in a model. It can be used by calling ```generate_model_size_information``` directly, or from the command line ``` buck2 run executorch/sdk/size_analysis_tool:size_analysis_tool -- --etrecord_path [...] --output_path [...] ``` Reviewed By: mcr229 Differential Revision: D48255922 fbshipit-source-id: 2d68bcb86ea347d82435bce095bc0669d3e889b5
1 parent 1363a0e commit d1563b6

File tree

3 files changed

+349
-0
lines changed

3 files changed

+349
-0
lines changed

sdk/size_analysis_tool/TARGETS

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
load("@fbcode_macros//build_defs:python_binary.bzl", "python_binary")
2+
load("@fbcode_macros//build_defs:python_library.bzl", "python_library")
3+
load("@fbcode_macros//build_defs:python_unittest.bzl", "python_unittest")
4+
5+
python_library(
6+
name = "size_analysis_tool_lib",
7+
srcs = [
8+
"size_analysis_tool.py",
9+
],
10+
visibility = ["PUBLIC"],
11+
deps = [
12+
"//caffe2:torch",
13+
"//executorch/exir:lib",
14+
"//executorch/exir/backend:backend_api",
15+
"//executorch/sdk/etrecord:etrecord",
16+
],
17+
)
18+
19+
python_binary(
20+
name = "size_analysis_tool",
21+
srcs = [
22+
"size_analysis_tool.py",
23+
],
24+
main_module = "executorch.sdk.size_analysis_tool.size_analysis_tool",
25+
visibility = ["PUBLIC"],
26+
deps = [
27+
"//caffe2:torch",
28+
"//executorch/exir:lib",
29+
"//executorch/exir/backend:backend_api",
30+
"//executorch/sdk/etrecord:etrecord",
31+
],
32+
)
33+
34+
python_unittest(
35+
name = "size_analysis_tool_test",
36+
srcs = [
37+
"size_analysis_tool.py",
38+
"size_analysis_tool_test.py",
39+
],
40+
deps = [
41+
"//caffe2:torch",
42+
"//executorch/backends/xnnpack/partition:xnnpack_partitioner",
43+
"//executorch/backends/xnnpack/utils:xnnpack_utils",
44+
"//executorch/exir:lib",
45+
"//executorch/exir/backend:backend_api",
46+
"//executorch/exir/passes:spec_prop_pass",
47+
"//executorch/sdk/etrecord:etrecord",
48+
],
49+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import argparse
8+
import json
9+
from typing import Any, Callable, Dict, List, Optional, Tuple
10+
11+
import torch
12+
13+
from executorch.exir import ExportedProgram
14+
from executorch.exir.backend.backend_api import LoweredBackendModule
15+
from executorch.sdk.etrecord import parse_etrecord
16+
from executorch.sdk.etrecord._etrecord import ETRecordReservedFileNames
17+
18+
19+
def _get_tensor_data(node: torch.fx.Node, tensor: torch.Tensor) -> Dict[str, Any]:
20+
return {
21+
"name": node.name,
22+
"numel": tensor.numel(),
23+
"dtype": str(tensor.dtype)[6:], # Remove "torch." prefix
24+
"element_size": tensor.element_size(),
25+
"shape": list(tensor.shape),
26+
"num_bytes": tensor.element_size() * tensor.numel(),
27+
"nn_module_stack": (
28+
str(node.meta["nn_module_stack"])
29+
if "nn_module_stack" in node.meta
30+
else None
31+
),
32+
}
33+
34+
35+
def _get_delegate_blob_data(
36+
node: torch.fx.Node,
37+
lowered_backend_module: LoweredBackendModule,
38+
delegate_deserializers: Optional[
39+
Dict[str, Callable[[bytes], Dict[str, Any]]]
40+
] = None,
41+
) -> Dict[str, Any]:
42+
delegate_blob_data = {
43+
"name": node.name,
44+
"backend_id": lowered_backend_module.backend_id,
45+
"num_bytes": len(lowered_backend_module.processed_bytes),
46+
}
47+
if (
48+
delegate_deserializers is not None
49+
and lowered_backend_module.backend_id in delegate_deserializers
50+
):
51+
delegate_blob_data.update(
52+
delegate_deserializers[lowered_backend_module.backend_id](
53+
lowered_backend_module.processed_bytes
54+
)
55+
)
56+
57+
return delegate_blob_data
58+
59+
60+
def _get_nested_model_data(
61+
graph_module: torch.fx.GraphModule,
62+
delegate_deserializers: Optional[
63+
Dict[str, Callable[[bytes], Dict[str, Any]]]
64+
] = None,
65+
tensor_data: Optional[List[Dict[str, Any]]] = None,
66+
delegate_blob_data: Optional[List[Dict[str, Any]]] = None,
67+
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
68+
if tensor_data is None:
69+
tensor_data = []
70+
71+
if delegate_blob_data is None:
72+
delegate_blob_data = []
73+
74+
for node in graph_module.graph.nodes:
75+
if node.op == "get_attr":
76+
node_attr = getattr(node.graph.owning_module, node.target)
77+
if isinstance(node_attr, torch.Tensor):
78+
tensor_data.append(_get_tensor_data(node, node_attr))
79+
elif isinstance(node_attr, torch.fx.GraphModule):
80+
_get_nested_model_data(
81+
node_attr, delegate_deserializers, tensor_data, delegate_blob_data
82+
)
83+
elif isinstance(node_attr, LoweredBackendModule):
84+
delegate_blob_data.append(
85+
_get_delegate_blob_data(node, node_attr, delegate_deserializers)
86+
)
87+
88+
return (tensor_data, delegate_blob_data)
89+
90+
91+
def generate_model_size_information(
92+
model: ExportedProgram,
93+
delegate_deserializers: Optional[
94+
Dict[str, Callable[[bytes], Dict[str, Any]]]
95+
] = None,
96+
flatbuffer: Optional[bytes] = None,
97+
) -> Dict[str, Any]:
98+
"""
99+
Generate a json-serializable Dict containing information about a model's
100+
size. This includes data about individual tensors and delegate blobs.
101+
Optionally:
102+
- delegate_deserializers can be provided to manually specify additional
103+
information to include for delegate blobs for specific backends.
104+
- flatbuffer can be provided to include a comparison of total tensor data
105+
size to overall model size
106+
"""
107+
108+
tensor_and_delegate_blob_data = _get_nested_model_data(
109+
model.graph_module, delegate_deserializers
110+
)
111+
112+
for data_list in tensor_and_delegate_blob_data:
113+
data_list.sort(key=lambda data: data["num_bytes"], reverse=True)
114+
115+
(tensor_data, delegate_blob_data) = tensor_and_delegate_blob_data
116+
117+
total_tensor_data_size = sum(data["num_bytes"] for data in tensor_data)
118+
total_delegate_blob_data_size = sum(
119+
data["num_bytes"] for data in delegate_blob_data
120+
)
121+
overview = {
122+
"total_tensor_data_size": total_tensor_data_size,
123+
"total_delegate_blob_data_size": total_delegate_blob_data_size,
124+
}
125+
if flatbuffer is not None:
126+
model_size = len(flatbuffer)
127+
overview.update(
128+
{
129+
"serialization_metadata_size": (
130+
model_size - total_tensor_data_size - total_delegate_blob_data_size
131+
),
132+
"model_size": model_size,
133+
}
134+
)
135+
136+
return {
137+
"tensor_data": tensor_data,
138+
"delegate_blob_data": delegate_blob_data,
139+
"overview": overview,
140+
}
141+
142+
143+
def parse_args():
144+
parser = argparse.ArgumentParser()
145+
146+
parser.add_argument(
147+
"--etrecord_path",
148+
required=True,
149+
help="The path to the ETRecord for the model to generate size information for",
150+
)
151+
152+
parser.add_argument(
153+
"--output_path",
154+
default="model_size_information.json",
155+
help="The output path for the model size information as a json file",
156+
)
157+
158+
args = parser.parse_args()
159+
return args
160+
161+
162+
def main():
163+
args = parse_args()
164+
165+
etrecord = parse_etrecord(args.etrecord_path)
166+
167+
all_model_size_information = [
168+
generate_model_size_information(
169+
model=exported_program,
170+
delegate_deserializers=None,
171+
flatbuffer=(
172+
etrecord.program_buffer
173+
if name == ETRecordReservedFileNames.ET_DIALECT_GRAPH_MODULE
174+
else None
175+
),
176+
)
177+
for (name, exported_program) in etrecord.graph_map.items()
178+
]
179+
180+
with open(args.output_path, "w") as f:
181+
f.write(json.dumps(all_model_size_information))
182+
183+
184+
if __name__ == "__main__":
185+
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import unittest
8+
9+
import torch
10+
from executorch.backends.xnnpack.partition.xnnpack_partitioner import (
11+
XnnpackFloatingPointPartitioner,
12+
)
13+
from executorch.backends.xnnpack.utils.configs import (
14+
get_xnnpack_executorch_backend_config,
15+
)
16+
from executorch.backends.xnnpack.utils.utils import capture_graph_for_xnnpack
17+
from executorch.exir.backend.backend_api import to_backend, validation_disabled
18+
from executorch.exir.passes.spec_prop_pass import SpecPropPass
19+
20+
from executorch.sdk.size_analysis_tool.size_analysis_tool import (
21+
generate_model_size_information,
22+
)
23+
24+
25+
class SizeAnalysisToolTest(unittest.TestCase):
26+
def test_generate_model_size_analysis(self):
27+
class MyModel(torch.nn.Module):
28+
def __init__(self):
29+
super().__init__()
30+
self.sigmoid = torch.nn.Sigmoid()
31+
self.conv3d = torch.nn.Conv3d(
32+
in_channels=4, out_channels=2, kernel_size=3
33+
)
34+
self.conv2d = torch.nn.Conv2d(
35+
in_channels=5,
36+
out_channels=2,
37+
kernel_size=3,
38+
)
39+
self.conv_transpose2d = torch.nn.ConvTranspose2d(
40+
in_channels=2, out_channels=4, kernel_size=2
41+
)
42+
43+
def forward(self, x):
44+
x = self.sigmoid(x)
45+
x = self.conv3d(x)
46+
x = self.conv2d(x)
47+
x = self.conv_transpose2d(x)
48+
return x
49+
50+
mm = MyModel()
51+
mm.eval()
52+
53+
test_input = torch.ones(size=(4, 7, 5, 6), dtype=torch.float)
54+
55+
edge_program = capture_graph_for_xnnpack(mm, (test_input,))
56+
partitioner = XnnpackFloatingPointPartitioner
57+
58+
with validation_disabled():
59+
delegated_program = edge_program
60+
delegated_program.exported_program = to_backend(
61+
edge_program.exported_program, partitioner
62+
)
63+
64+
program = delegated_program.to_executorch(
65+
get_xnnpack_executorch_backend_config([SpecPropPass()]),
66+
)
67+
68+
size_information = generate_model_size_information(
69+
model=program,
70+
delegate_deserializers=None,
71+
flatbuffer=program.buffer,
72+
)
73+
74+
# Number of Elements -> Other tensor data
75+
exepected_tensor_data = {
76+
# Conv3d Weight
77+
216: {
78+
"dtype": "float32",
79+
"element_size": 4,
80+
"shape": [2, 4, 3, 3, 3],
81+
"num_bytes": 864,
82+
},
83+
# ConvTranspose2d Weight
84+
32: {
85+
"dtype": "float32",
86+
"element_size": 4,
87+
"shape": [2, 4, 2, 2],
88+
"num_bytes": 128,
89+
},
90+
# ConvTranspose2d Bias
91+
4: {
92+
"dtype": "float32",
93+
"element_size": 4,
94+
"shape": [4],
95+
"num_bytes": 16,
96+
},
97+
# Conv3d Bias
98+
2: {
99+
"dtype": "float32",
100+
"element_size": 4,
101+
"shape": [2],
102+
"num_bytes": 8,
103+
},
104+
}
105+
106+
self.assertEqual(
107+
len(size_information["tensor_data"]), len(exepected_tensor_data)
108+
)
109+
110+
for tensor in size_information["tensor_data"]:
111+
for (k, v) in exepected_tensor_data[tensor["numel"]].items():
112+
self.assertEqual(tensor[k], v)
113+
114+
# Two delegate blobs: sigmoid and conv2d
115+
self.assertEqual(len(size_information["delegate_blob_data"]), 2)

0 commit comments

Comments
 (0)