Skip to content

Commit 3c4b95f

Browse files
Make FCB lowering and cleanup safe (pytorch#24159)
Summary: ## Summary Flexible Context Binary (FCB) compilation already existed behind the multimethod path, but a single `ExportedProgram` passed through `BackendDetails.preprocess()` still took the scalar path. This made the standard `to_backend()` API unable to produce a multi-target QNN artifact safely. This change: - routes FCB compiler specs from `QnnBackend.preprocess()` through `preprocess_multimethod()`; - scopes `QnnManager` reuse to `QnnManagerContext`, releases every manager, QNN context, and DLC handle on success or failure, and preserves every teardown failure; - validates empty, unequal, duplicate, unsupported, and incompatible multi-target specs while preserving every one-target scalar/list input form; - keeps DLBC plus reference-weight sharing advisory rather than rejecting it; - warns when v6x and v7x-or-newer HTP targets are combined because weight sharing may be less effective; - builds debug mappings after wrapper construction assigns tensor names; - adds public QNN CI coverage for compiler specs, lifecycle, preprocessing, protocol stripping, and cleanup. The scalar path remains unchanged. A one-target list still produces a scalar context binary. ## Authorship Authored with Codex. Differential Revision: D119582979
1 parent 5c788db commit 3c4b95f

8 files changed

Lines changed: 646 additions & 36 deletions

File tree

‎.github/workflows/pull.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,12 @@ jobs:
13031303
pytest -vs backends/qualcomm/tests/test_qnn_delegate.py \
13041304
-k "TestQNNFloatingPointOperator or TestQNNQuantizedOperator"
13051305
1306+
# Run focused FCB compiler-contract, lifecycle, and preprocessing tests.
1307+
pytest -vs \
1308+
backends/qualcomm/tests/test_qnn_compiler_spec.py \
1309+
backends/qualcomm/tests/test_qnn_manager_lifecycle.py \
1310+
backends/qualcomm/tests/test_qnn_preprocess_fcb.py
1311+
13061312
test-phi-3-mini-runner-linux:
13071313
needs: docker-image
13081314
name: test-phi-3-mini-runner-linux

‎backends/qualcomm/qnn_preprocess.py‎

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -158,32 +158,50 @@ def preprocess(
158158
) -> PreprocessResult:
159159
option = generate_qnn_executorch_option(compile_specs)
160160
obj_options = flatbuffer_to_option(option)
161+
if obj_options.target_options is not None:
162+
return QnnBackend.preprocess_multimethod(
163+
{DEFAULT_GRAPH_NAME: [edge_program]},
164+
{DEFAULT_GRAPH_NAME: [compile_specs]},
165+
)[DEFAULT_GRAPH_NAME][0]
161166
qnn_manager = get_current_qnn_manager(compile_specs)
162-
qnn_manager.InitContext([DEFAULT_GRAPH_NAME])
163-
py_op_wrapper_list = QnnBackend._build_op_wrappers(
164-
edge_program,
165-
qnn_manager.IsTensorDump(),
166-
obj_options.op_package_options.op_package_infos,
167-
obj_options.use_mha2sha,
168-
obj_options.backend_options.backend_type,
169-
)
167+
init_result = qnn_manager.InitContext([DEFAULT_GRAPH_NAME])
168+
try:
169+
if init_result.value != 0:
170+
raise RuntimeError(
171+
f"Failed to initialize QNN context: {init_result.value}"
172+
)
173+
py_op_wrapper_list = QnnBackend._build_op_wrappers(
174+
edge_program,
175+
qnn_manager.IsTensorDump(),
176+
obj_options.op_package_options.op_package_infos,
177+
obj_options.use_mha2sha,
178+
obj_options.backend_options.backend_type,
179+
)
170180

171-
qnn_context_binary = qnn_manager.Compile(
172-
qnn_manager.GetGraphNames(),
173-
[[py_op_wrapper.GetOpWrapper() for py_op_wrapper in py_op_wrapper_list]],
174-
)
181+
qnn_context_binary = qnn_manager.Compile(
182+
qnn_manager.GetGraphNames(),
183+
[
184+
[
185+
py_op_wrapper.GetOpWrapper()
186+
for py_op_wrapper in py_op_wrapper_list
187+
]
188+
],
189+
)
175190

176-
if obj_options.saver:
177-
exit(
178-
f"Record all QNN API calls from saver backend at: {obj_options.saver_output_dir}"
191+
if obj_options.saver:
192+
exit(
193+
f"Record all QNN API calls from saver backend at: {obj_options.saver_output_dir}"
194+
)
195+
assert (
196+
len(qnn_context_binary) != 0
197+
), "Failed to generate Qnn context binary."
198+
# For now, debug_handle_map is not used by QNN ExecuTorch
199+
return PreprocessResult(
200+
processed_bytes=qnn_context_binary,
201+
debug_handle_map={},
179202
)
180-
assert len(qnn_context_binary) != 0, "Failed to generate Qnn context binary."
181-
qnn_manager.DestroyContext()
182-
# For now, debug_handle_map is not used by QNN ExecuTorch
183-
return PreprocessResult(
184-
processed_bytes=qnn_context_binary,
185-
debug_handle_map={},
186-
)
203+
finally:
204+
qnn_manager.DestroyContext()
187205

188206
@staticmethod
189207
def _populate_delegate_mapping(
@@ -207,8 +225,12 @@ def _populate_delegate_mapping(
207225
@staticmethod
208226
def _get_compile_func(qnn_manager: PyQnnManager.QnnManager):
209227
def compile_func(graph_names, op_wrapper_list):
210-
qnn_manager.InitContext(graph_names)
228+
init_result = qnn_manager.InitContext(graph_names)
211229
try:
230+
if init_result.value != 0:
231+
raise RuntimeError(
232+
f"Failed to initialize QNN context: {init_result.value}"
233+
)
212234
qnn_context_binary = qnn_manager.Compile(graph_names, op_wrapper_list)
213235
finally:
214236
qnn_manager.DestroyContext()
@@ -221,8 +243,13 @@ def _get_compile_func_fcb(qnn_managers: List[PyQnnManager.QnnManager]):
221243
def compile_func(graph_names, op_wrapper_list):
222244
with qnn_managers[0].CreateDlc() as dlc_handle:
223245
for qnn_manager in qnn_managers:
224-
qnn_manager.InitContext(graph_names)
246+
init_result = qnn_manager.InitContext(graph_names)
225247
try:
248+
if init_result.value != 0:
249+
raise RuntimeError(
250+
"Failed to initialize QNN context: "
251+
f"{init_result.value}"
252+
)
226253
qnn_manager.CompileToDlc(
227254
graph_names, op_wrapper_list, dlc_handle
228255
)

‎backends/qualcomm/tests/BUCK‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ load("@fbsource//xplat/executorch/backends/qualcomm/qnn_version.bzl", "get_qnn_l
55

66
oncall("executorch")
77

8+
_QNN = "fbsource//third-party/qualcomm/qnn/qnn-{0}".format(get_qnn_library_version())
9+
_QNN_ROOT = "$(location {0}:__dir__)".format(_QNN)
10+
_QNN_HOST_LIBS = _QNN_ROOT + "/lib/x86_64-linux-clang"
11+
812
fbcode_target(_kind = runtime.python_library,
913
name = "models",
1014
srcs = ["models.py"],
@@ -135,3 +139,44 @@ None if runtime.is_oss else fbcode_target(
135139
":test_qnn_delegate",
136140
],
137141
)
142+
143+
# Compiler-contract and manager-lifecycle coverage for Flexible Context Binary export.
144+
fbcode_target(
145+
_kind = runtime.python_test,
146+
name = "test_qnn_compiler_spec",
147+
srcs = ["test_qnn_compiler_spec.py"],
148+
env = {} if runtime.is_oss else {
149+
"LD_LIBRARY_PATH": _QNN_HOST_LIBS,
150+
"QNN_SDK_ROOT": _QNN_ROOT,
151+
},
152+
deps = [
153+
":test_qnn_delegate",
154+
],
155+
)
156+
157+
fbcode_target(
158+
_kind = runtime.python_test,
159+
name = "test_qnn_manager_lifecycle",
160+
srcs = ["test_qnn_manager_lifecycle.py"],
161+
env = {} if runtime.is_oss else {
162+
"LD_LIBRARY_PATH": _QNN_HOST_LIBS,
163+
"QNN_SDK_ROOT": _QNN_ROOT,
164+
},
165+
deps = [
166+
":test_qnn_delegate",
167+
],
168+
)
169+
170+
fbcode_target(
171+
_kind = runtime.python_test,
172+
name = "test_qnn_preprocess_fcb",
173+
srcs = ["test_qnn_preprocess_fcb.py"],
174+
env = {} if runtime.is_oss else {
175+
"LD_LIBRARY_PATH": _QNN_HOST_LIBS,
176+
"QNN_SDK_ROOT": _QNN_ROOT,
177+
},
178+
deps = [
179+
":test_qnn_delegate",
180+
"//executorch/backends/qualcomm/aot/python:PyQnnManagerAdaptor",
181+
],
182+
)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
from __future__ import annotations
8+
9+
import unittest
10+
11+
from executorch.backends.qualcomm.serialization.qc_schema import (
12+
QcomChipset,
13+
QnnExecuTorchBackendType,
14+
)
15+
from executorch.backends.qualcomm.serialization.qc_schema_serialize import (
16+
flatbuffer_to_option,
17+
)
18+
from executorch.backends.qualcomm.utils.utils import (
19+
generate_gpu_compiler_spec,
20+
generate_htp_compiler_spec,
21+
generate_qnn_executorch_compiler_spec,
22+
)
23+
24+
25+
class QnnCompilerSpecTest(unittest.TestCase):
26+
def test_fcb_pairs_soc_and_htp_options(self):
27+
first = generate_htp_compiler_spec(use_fp16=False)
28+
second = generate_htp_compiler_spec(use_fp16=True)
29+
option = flatbuffer_to_option(
30+
generate_qnn_executorch_compiler_spec(
31+
soc_model=[QcomChipset.SM8650, QcomChipset.SM8750],
32+
backend_options=[first, second],
33+
)[0].value
34+
)
35+
36+
self.assertTrue(option.target_options.fcb_reference_weight_sharing)
37+
self.assertEqual(
38+
[target.soc_info.soc_model for target in option.target_options.targets],
39+
[QcomChipset.SM8650, QcomChipset.SM8750],
40+
)
41+
self.assertEqual(
42+
[
43+
target.backend_options[0].htp_options.precision
44+
for target in option.target_options.targets
45+
],
46+
[first.htp_options.precision, second.htp_options.precision],
47+
)
48+
49+
def test_fcb_rejects_invalid_target_configuration(self):
50+
htp = generate_htp_compiler_spec(use_fp16=False)
51+
cases = [
52+
(
53+
{
54+
"soc_model": [],
55+
"backend_options": [],
56+
},
57+
"must not be empty",
58+
),
59+
(
60+
{
61+
"soc_model": [QcomChipset.SM8650, QcomChipset.SM8750],
62+
"backend_options": [htp],
63+
},
64+
"equal-length",
65+
),
66+
(
67+
{
68+
"soc_model": [QcomChipset.SM8650, QcomChipset.SM8750],
69+
"backend_options": [htp, generate_gpu_compiler_spec()],
70+
},
71+
"HTP",
72+
),
73+
(
74+
{
75+
"soc_model": [QcomChipset.SM8650, QcomChipset.SM8750],
76+
"backend_options": [htp, htp],
77+
"online_prepare": True,
78+
},
79+
"offline_prepare",
80+
),
81+
(
82+
{
83+
"soc_model": QcomChipset.SM8650,
84+
"backend_options": [htp, htp],
85+
},
86+
"equal-length",
87+
),
88+
(
89+
{
90+
"soc_model": [QcomChipset.SM8650, QcomChipset.SM8750],
91+
"backend_options": htp,
92+
},
93+
"equal-length",
94+
),
95+
]
96+
for kwargs, message in cases:
97+
with self.subTest(kwargs=kwargs), self.assertRaisesRegex(
98+
ValueError, message
99+
):
100+
generate_qnn_executorch_compiler_spec(**kwargs)
101+
102+
def test_fcb_warns_for_dlbc_when_reference_sharing_enabled(self):
103+
htp = generate_htp_compiler_spec(use_fp16=False, use_dlbc=True)
104+
with self.assertWarnsRegex(UserWarning, "DLBC"):
105+
specs = generate_qnn_executorch_compiler_spec(
106+
soc_model=[QcomChipset.SM8650, QcomChipset.SM8750],
107+
backend_options=[htp, htp],
108+
)
109+
self.assertGreater(len(specs), 0)
110+
111+
def test_single_target_remains_non_fcb(self):
112+
htp = generate_htp_compiler_spec(use_fp16=False)
113+
for soc_model, backend_options in (
114+
(QcomChipset.SM8650, htp),
115+
([QcomChipset.SM8650], [htp]),
116+
(QcomChipset.SM8650, [htp]),
117+
([QcomChipset.SM8650], htp),
118+
):
119+
with self.subTest(soc_model=soc_model):
120+
option = flatbuffer_to_option(
121+
generate_qnn_executorch_compiler_spec(
122+
soc_model=soc_model,
123+
backend_options=backend_options,
124+
)[0].value
125+
)
126+
self.assertIsNone(option.target_options)
127+
self.assertEqual(
128+
option.backend_options.backend_type,
129+
QnnExecuTorchBackendType.kHtpBackend,
130+
)

0 commit comments

Comments
 (0)