Skip to content

[Backend Tester] Report PTE size #13249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 96 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from 93 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
f120e70
Update
GregoryComer Jul 18, 2025
0fb85e6
Update
GregoryComer Jul 18, 2025
4d8d844
Update
GregoryComer Jul 19, 2025
dc12b40
Update
GregoryComer Jul 21, 2025
ead0616
Update
GregoryComer Jul 22, 2025
0f13676
Update
GregoryComer Jul 22, 2025
b0b01f2
Update
GregoryComer Jul 22, 2025
8b9c9ef
Update
GregoryComer Jul 22, 2025
06bf03a
Update
GregoryComer Jul 22, 2025
2f8f49b
Update
GregoryComer Jul 22, 2025
8ca7766
Update
GregoryComer Jul 22, 2025
bffb95f
Update
GregoryComer Jul 22, 2025
d21492b
Update
GregoryComer Jul 22, 2025
e2c4ea5
Update
GregoryComer Jul 22, 2025
8230848
Update
GregoryComer Jul 22, 2025
2a1f564
Update
GregoryComer Jul 22, 2025
b35e7b1
Update
GregoryComer Jul 22, 2025
5c4c6ce
Update
GregoryComer Jul 22, 2025
9397803
Update
GregoryComer Jul 22, 2025
9dfeb5a
Update
GregoryComer Jul 22, 2025
ff5c4a5
Update
GregoryComer Jul 22, 2025
42a5de5
Update
GregoryComer Jul 22, 2025
402d8f5
Update
GregoryComer Jul 22, 2025
34d3ab3
Update
GregoryComer Jul 22, 2025
1105e04
Update
GregoryComer Jul 22, 2025
482bd21
Update
GregoryComer Jul 22, 2025
ea548b7
Update
GregoryComer Jul 23, 2025
4108f54
Update
GregoryComer Jul 23, 2025
7ef236b
Update
GregoryComer Jul 23, 2025
4a58c9d
Update
GregoryComer Jul 23, 2025
3b866b4
Update
GregoryComer Jul 23, 2025
5ba25cb
Update
GregoryComer Jul 23, 2025
19760fc
Update
GregoryComer Jul 23, 2025
81dfb07
Update
GregoryComer Jul 23, 2025
4d50265
Update
GregoryComer Jul 23, 2025
5f66043
Update
GregoryComer Jul 23, 2025
24e919d
Update
GregoryComer Jul 23, 2025
523cc20
Update
GregoryComer Jul 23, 2025
74c95fe
Update
GregoryComer Jul 23, 2025
5d437b1
Update
GregoryComer Jul 23, 2025
89757ce
Update
GregoryComer Jul 23, 2025
423f79a
Update
GregoryComer Jul 23, 2025
69f7f9c
Update
GregoryComer Jul 23, 2025
c0f6224
Update
GregoryComer Jul 23, 2025
e2ea2a3
Update
GregoryComer Jul 23, 2025
7a2fab5
Update
GregoryComer Jul 23, 2025
033c231
Update
GregoryComer Jul 23, 2025
a9ed762
Update
GregoryComer Jul 23, 2025
64b174a
Update
GregoryComer Jul 23, 2025
3976629
Update
GregoryComer Jul 23, 2025
27cd171
Update
GregoryComer Jul 23, 2025
7bdd3e5
Update
GregoryComer Jul 23, 2025
b1254cd
Update
GregoryComer Jul 23, 2025
f2e2289
Update
GregoryComer Jul 23, 2025
cdd15c1
Update
GregoryComer Jul 23, 2025
e2df06e
Update
GregoryComer Jul 23, 2025
4461bd8
Update
GregoryComer Jul 23, 2025
7e97fd0
Update
GregoryComer Jul 23, 2025
bcb697c
Update
GregoryComer Jul 23, 2025
11a5a02
Update
GregoryComer Jul 24, 2025
244b146
Update
GregoryComer Jul 24, 2025
de21ac2
Update
GregoryComer Jul 24, 2025
fd26fc7
Update
GregoryComer Jul 24, 2025
4ae840d
Update
GregoryComer Jul 24, 2025
710ea49
Update
GregoryComer Jul 24, 2025
32f54b0
Update
GregoryComer Jul 24, 2025
a27d18c
Update
GregoryComer Jul 24, 2025
2eb59fc
Update
GregoryComer Jul 24, 2025
5cc4941
Update
GregoryComer Jul 24, 2025
ef7af5c
Update
GregoryComer Jul 24, 2025
18e89c1
Update
GregoryComer Jul 24, 2025
4719c90
Update
GregoryComer Jul 25, 2025
dd09555
Update
GregoryComer Aug 8, 2025
f1db3a0
Update
GregoryComer Aug 8, 2025
e0700b2
Update
GregoryComer Aug 8, 2025
f260b50
Update
GregoryComer Aug 8, 2025
d62ee60
Update
GregoryComer Aug 8, 2025
b2ab3a5
Update
GregoryComer Aug 8, 2025
f261355
Update
GregoryComer Aug 11, 2025
c3a24f9
Update
GregoryComer Aug 11, 2025
1697cbc
Update
GregoryComer Aug 11, 2025
b94b45e
Update
GregoryComer Aug 11, 2025
5740f0a
Update
GregoryComer Aug 11, 2025
ed6840d
Update
GregoryComer Aug 11, 2025
bd79ef2
Update
GregoryComer Aug 12, 2025
8932c29
Update
GregoryComer Aug 12, 2025
7e1a002
Update
GregoryComer Aug 12, 2025
a628d29
Update
GregoryComer Aug 12, 2025
3615d89
Update
GregoryComer Aug 12, 2025
e994bc1
Update
GregoryComer Aug 12, 2025
1d34f49
Update
GregoryComer Aug 12, 2025
933fba2
Update
GregoryComer Aug 12, 2025
d468ae4
Update
GregoryComer Aug 12, 2025
1897d4e
Update
GregoryComer Aug 12, 2025
f65d80f
Update
GregoryComer Aug 12, 2025
871312a
Update
GregoryComer Aug 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backends/qualcomm/tests/tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def __init__(
default_partitioner_cls=QnnPartitioner,
)

def run(self, artifact: ExportedProgram, inputs=None) -> None:
def run(
self, artifact: ExportedProgram, inputs=None, generate_etrecord: bool = False
) -> None:
ep = QnnPassManager().transform_for_export_pipeline(artifact)
transform_passes = QnnPassManager().get_to_edge_transform_passes(ep)

Expand All @@ -61,6 +63,7 @@ def run(self, artifact: ExportedProgram, inputs=None) -> None:
transform_passes=transform_passes,
partitioner=self.partitioners,
compile_config=self.edge_compile_conf,
generate_etrecord=generate_etrecord,
)


Expand Down
6 changes: 5 additions & 1 deletion backends/test/harness/stages/to_edge_transform_and_lower.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
to_edge_transform_and_lower,
)
from executorch.exir.backend.partitioner import Partitioner

from torch.export import ExportedProgram


Expand All @@ -24,11 +25,14 @@ def __init__(
def stage_type(self) -> StageType:
return StageType.TO_EDGE_TRANSFORM_AND_LOWER

def run(self, artifact: ExportedProgram, inputs=None) -> None:
def run(
self, artifact: ExportedProgram, inputs=None, generate_etrecord: bool = False
) -> None:
self.edge_dialect_program = to_edge_transform_and_lower(
artifact,
compile_config=self.edge_compile_conf,
partitioner=self.partitioners,
generate_etrecord=generate_etrecord,
)

@property
Expand Down
11 changes: 7 additions & 4 deletions backends/test/harness/tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,10 @@ def _post(self, stage):
assert stage_type in self.stages
self.stages[stage_type] = stage

def _run_stage(self, stage_instance, inputs=None):
def _run_stage(self, stage_instance, inputs=None, *args, **kwargs):
assert isinstance(stage_instance, Stage)
prev_stage_artifact = self._pre(stage_instance)
stage_instance.run(prev_stage_artifact, inputs=inputs)
stage_instance.run(prev_stage_artifact, inputs=inputs, *args, **kwargs) # noqa
self._post(stage_instance)
return self

Expand All @@ -213,11 +213,14 @@ def to_edge(self, to_edge_stage: Optional[ToEdge] = None):
return res

def to_edge_transform_and_lower(
self, to_edge_and_transform_stage: Optional[ToEdgeTransformAndLower] = None
self,
to_edge_and_transform_stage: Optional[ToEdgeTransformAndLower] = None,
generate_etrecord: bool = False,
):
return self._run_stage(
to_edge_and_transform_stage
or self._get_default_stage(StageType.TO_EDGE_TRANSFORM_AND_LOWER)
or self._get_default_stage(StageType.TO_EDGE_TRANSFORM_AND_LOWER),
generate_etrecord=generate_etrecord,
)

def run_passes(self, run_passes_stage: Optional[RunPasses] = None):
Expand Down
105 changes: 104 additions & 1 deletion backends/test/suite/reporting.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import csv

from collections import Counter
from dataclasses import dataclass
from datetime import timedelta
from enum import IntEnum
from functools import reduce
from typing import TextIO
from typing import Any, TextIO

from executorch.backends.test.harness.error_statistics import ErrorStatistics
from torch.export import ExportedProgram


# Operators that are excluded from the counts returned by count_ops. These are used to
# exclude operatations that are not logically relevant or delegatable to backends.
OP_COUNT_IGNORED_OPS = {
"executorch_call_delegate",
"getitem",
}


class TestResult(IntEnum):
Expand Down Expand Up @@ -108,6 +119,21 @@ class TestCaseSummary:
a single output tensor.
"""

quantize_time: timedelta | None = None
""" The total runtime of the quantization stage, or none, if the test did not run the quantize stage. """

lower_time: timedelta | None = None
""" The total runtime of the to_edge_transform_and_lower stage, or none, if the test did not run the quantize stage. """

delegated_op_counts: Counter | None = None
""" The number of delegated occurances of each operator in the graph. """

undelegated_op_counts: Counter | None = None
""" The number of undelegated occurances of each operator in the graph. """

pte_size_bytes: int | None = None
""" The size of the PTE file in bytes. """


class TestSessionState:
test_case_summaries: list[TestCaseSummary]
Expand Down Expand Up @@ -157,6 +183,40 @@ def from_session(cls, session: TestSessionState) -> "RunSummary":
_active_session: TestSessionState | None = None


def _get_target_name(target: Any) -> str:
"""Retrieve a string representation of a node target."""
if isinstance(target, str):
return target
elif hasattr(target, "name"):
return target.name() # Op overloads have this
elif hasattr(target, "__name__"):
return target.__name__ # Some builtins have this
else:
return str(target)


def _count_ops(program: ExportedProgram) -> Counter:
op_names = (
_get_target_name(n.target)
for n in program.graph.nodes
if n.op == "call_function"
)

return Counter(op for op in op_names if op not in OP_COUNT_IGNORED_OPS)


def count_ops(program: dict[str, ExportedProgram] | ExportedProgram) -> Counter:
if isinstance(program, ExportedProgram):
return _count_ops(program)
else:
# Sum op counts for all methods in the program.
return reduce(
lambda a, b: a + b,
(_count_ops(p) for p in program.values()),
Counter(),
)


def begin_test_session():
global _active_session

Expand All @@ -181,6 +241,24 @@ def complete_test_session() -> RunSummary:
return summary


def _sum_op_counts(counter: Counter | None) -> int | None:
"""
A utility function to count the total number of nodes in an op count dict.
"""
return sum(counter.values()) if counter is not None else None


def _serialize_op_counts(counter: Counter | None) -> str:
"""
A utility function to serialize op counts to a string, for the purpose of including
in the test report.
"""
if counter is not None:
return str(dict(sorted(counter.items())))
else:
return ""


def generate_csv_report(summary: RunSummary, output: TextIO):
"""Write a run summary report to a file in CSV format."""

Expand All @@ -190,6 +268,8 @@ def generate_csv_report(summary: RunSummary, output: TextIO):
"Backend",
"Flow",
"Result",
"Quantize Time (s)",
"Lowering Time (s)",
]

# Tests can have custom parameters. We'll want to report them here, so we need
Expand Down Expand Up @@ -219,6 +299,15 @@ def generate_csv_report(summary: RunSummary, output: TextIO):
f"Output {i} SQNR",
]
)
field_names.extend(
[
"Delegated Nodes",
"Undelegated Nodes",
"Delegated Ops",
"Undelegated Ops",
"PTE Size (Kb)",
]
)

writer = csv.DictWriter(output, field_names)
writer.writeheader()
Expand All @@ -230,6 +319,12 @@ def generate_csv_report(summary: RunSummary, output: TextIO):
"Backend": record.backend,
"Flow": record.flow,
"Result": record.result.display_name(),
"Quantize Time (s)": (
record.quantize_time.total_seconds() if record.quantize_time else None
),
"Lowering Time (s)": (
record.lower_time.total_seconds() if record.lower_time else None
),
}
if record.params is not None:
row.update({k.capitalize(): v for k, v in record.params.items()})
Expand All @@ -241,4 +336,12 @@ def generate_csv_report(summary: RunSummary, output: TextIO):
row[f"Output {output_idx} Error L2"] = error_stats.error_l2_norm
row[f"Output {output_idx} SQNR"] = error_stats.sqnr

row["Delegated Nodes"] = _sum_op_counts(record.delegated_op_counts)
row["Undelegated Nodes"] = _sum_op_counts(record.undelegated_op_counts)
row["Delegated Ops"] = _serialize_op_counts(record.delegated_op_counts)
row["Undelegated Ops"] = _serialize_op_counts(record.undelegated_op_counts)
row["PTE Size (Kb)"] = (
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit use 1024 and call it KiB? ;P

record.pte_size_bytes / 1000.0 if record.pte_size_bytes else ""
)

writer.writerow(row)
31 changes: 30 additions & 1 deletion backends/test/suite/runner.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import argparse
import importlib
import re
import time
import unittest

from datetime import timedelta
from typing import Any

import torch
Expand All @@ -14,11 +16,13 @@
from executorch.backends.test.suite.reporting import (
begin_test_session,
complete_test_session,
count_ops,
generate_csv_report,
RunSummary,
TestCaseSummary,
TestResult,
)
from executorch.exir import EdgeProgramManager


# A list of all runnable test suites and the corresponding python package.
Expand All @@ -44,6 +48,7 @@ def run_test( # noqa: C901
"""

error_statistics: list[ErrorStatistics] = []
extra_stats = {}

# Helper method to construct the summary.
def build_result(
Expand All @@ -58,6 +63,7 @@ def build_result(
result=result,
error=error,
tensor_error_statistics=error_statistics,
**extra_stats,
)

# Ensure the model can run in eager mode.
Expand All @@ -72,11 +78,16 @@ def build_result(
return build_result(TestResult.UNKNOWN_FAIL, e)

if flow.quantize:
start_time = time.perf_counter()
try:
tester.quantize(
flow.quantize_stage_factory() if flow.quantize_stage_factory else None
)
elapsed = time.perf_counter() - start_time
extra_stats["quantize_time"] = timedelta(seconds=elapsed)
except Exception as e:
elapsed = time.perf_counter() - start_time
extra_stats["quantize_time"] = timedelta(seconds=elapsed)
return build_result(TestResult.QUANTIZE_FAIL, e)

try:
Expand All @@ -87,11 +98,27 @@ def build_result(
except Exception as e:
return build_result(TestResult.EXPORT_FAIL, e)

lower_start_time = time.perf_counter()
try:
tester.to_edge_transform_and_lower()
tester.to_edge_transform_and_lower(generate_etrecord=True)
elapsed = time.perf_counter() - lower_start_time
extra_stats["lower_time"] = timedelta(seconds=elapsed)
except Exception as e:
elapsed = time.perf_counter() - lower_start_time
extra_stats["lower_time"] = timedelta(seconds=elapsed)
return build_result(TestResult.LOWER_FAIL, e)

# Compute delegation statistics. Use the ETRecord to access the edge dialect graph between
# to_edge and delegation. Note that ETRecord only stores the edge dialect graph for a single
# method currently and assumes it is called "forward".
edge_manager: EdgeProgramManager = tester.get_artifact()
edge_op_counts = count_ops({"forward": edge_manager._etrecord.edge_dialect_program})
undelegated_op_counts = count_ops(edge_manager._edge_programs)
delegated_op_counts = edge_op_counts - undelegated_op_counts

extra_stats["delegated_op_counts"] = delegated_op_counts
extra_stats["undelegated_op_counts"] = undelegated_op_counts

is_delegated = any(
n.target == torch._higher_order_ops.executorch_call_delegate
for n in tester.stages[tester.cur].graph_module.graph.nodes
Expand All @@ -102,6 +129,7 @@ def build_result(
if is_delegated:
try:
tester.to_executorch().serialize()
extra_stats["pte_size_bytes"] = len(tester.get_artifact())
except Exception as e:
# We could introduce a result value for this, but I'm not sure it's necessary.
# We can do this if we ever see to_executorch() or serialize() fail due a backend issue.
Expand Down Expand Up @@ -185,6 +213,7 @@ def parse_args():
"--report",
nargs="?",
help="A file to write the test report to, in CSV format.",
default="backend_test_report.csv",
)
return parser.parse_args()

Expand Down
Loading
Loading