Skip to content

Commit f5acbdb

Browse files
authored
Arm backend: Add a repro command when VGF model-converter fails (#20443)
Add a repro command when VGF model-converter fails. This is the fisrt part of the project to create unified bundle in case of failure. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Elena Zhelezina <elena.zhelezina@arm.com>
1 parent e98e74e commit f5acbdb

2 files changed

Lines changed: 248 additions & 5 deletions

File tree

backends/arm/test/misc/test_vgf_backend.py

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
# This source code is licensed under the BSD-style license found in the
44
# LICENSE file in the root directory of this source tree.
55

6+
import os
67
from types import SimpleNamespace
78
from typing import cast
9+
from unittest import mock
810

911
import pytest
1012

@@ -14,7 +16,14 @@
1416
clear_registered_pass_insertions,
1517
PassInsertions,
1618
)
17-
from executorch.backends.arm.vgf import backend as vgf_backend, VgfCompileSpec
19+
20+
from executorch.backends.arm.vgf import backend, backend as vgf_backend, VgfCompileSpec
21+
from executorch.backends.arm.vgf.backend import (
22+
_copy_failure_artifacts,
23+
_format_repro_command,
24+
_replace_converter_input_path,
25+
vgf_compile,
26+
)
1827
from executorch.exir.backend.backend_details import PreprocessResult
1928
from executorch.exir.pass_base import ExportPass
2029
from torch.export.exported_program import ExportedProgram
@@ -105,3 +114,180 @@ def _raise(*args, **kwargs):
105114
assert _registry_state() == original_registry
106115
finally:
107116
clear_registered_pass_insertions()
117+
118+
119+
def test_format_repro_command_quotes_shell_metacharacters():
120+
command = [
121+
"model-converter",
122+
"--flag=value with spaces",
123+
"-i",
124+
"input file.tosa",
125+
"-o",
126+
"output file.vgf",
127+
]
128+
129+
formatted = _format_repro_command(command)
130+
131+
assert formatted == (
132+
"model-converter "
133+
"'--flag=value with spaces' "
134+
"-i "
135+
"'input file.tosa' "
136+
"-o "
137+
"'output file.vgf'"
138+
)
139+
140+
141+
def test_replace_converter_input_path_replaces_input_after_i():
142+
command = [
143+
"model-converter",
144+
"--some-flag",
145+
"-i",
146+
"original.tosa",
147+
"-o",
148+
"output.vgf",
149+
]
150+
151+
replaced = _replace_converter_input_path(command, "preserved.tosa")
152+
153+
assert replaced == [
154+
"model-converter",
155+
"--some-flag",
156+
"-i",
157+
"preserved.tosa",
158+
"-o",
159+
"output.vgf",
160+
]
161+
assert command[3] == "original.tosa"
162+
163+
164+
def test_copy_failure_artifacts_returns_none_without_artifact_path(tmp_path):
165+
tosa_path = tmp_path / "input.tosa"
166+
tosa_path.write_bytes(b"tosa bytes")
167+
168+
copied_path = _copy_failure_artifacts(
169+
str(tosa_path),
170+
artifact_path=None,
171+
tag_name="delegate_0",
172+
)
173+
174+
assert copied_path is None
175+
176+
177+
def test_copy_failure_artifacts_copies_tosa_with_tag_name(tmp_path):
178+
tosa_path = tmp_path / "input.tosa"
179+
artifact_path = tmp_path / "artifacts"
180+
tosa_path.write_bytes(b"tosa bytes")
181+
182+
copied_path = _copy_failure_artifacts(
183+
str(tosa_path),
184+
str(artifact_path),
185+
tag_name="delegate_0",
186+
)
187+
188+
assert copied_path == os.path.join(
189+
str(artifact_path),
190+
"failed_model_converter_input_delegate_0.tosa",
191+
)
192+
assert os.path.exists(copied_path)
193+
assert open(copied_path, "rb").read() == b"tosa bytes"
194+
195+
196+
def test_copy_failure_artifacts_copies_tosa_without_tag_name(tmp_path):
197+
tosa_path = tmp_path / "input.tosa"
198+
artifact_path = tmp_path / "artifacts"
199+
tosa_path.write_bytes(b"tosa bytes")
200+
201+
copied_path = _copy_failure_artifacts(
202+
str(tosa_path),
203+
str(artifact_path),
204+
tag_name="",
205+
)
206+
207+
assert copied_path == os.path.join(
208+
str(artifact_path),
209+
"failed_model_converter_input.tosa",
210+
)
211+
assert os.path.exists(copied_path)
212+
assert open(copied_path, "rb").read() == b"tosa bytes"
213+
214+
215+
@mock.patch("executorch.backends.arm.vgf.backend.model_converter_env")
216+
@mock.patch("executorch.backends.arm.vgf.backend.require_model_converter_binary")
217+
@mock.patch("executorch.backends.arm.vgf.backend.subprocess.run")
218+
def test_vgf_compile_failure_includes_repro_command_and_copies_tosa(
219+
mock_run,
220+
mock_require_model_converter_binary,
221+
mock_model_converter_env,
222+
tmp_path,
223+
):
224+
artifact_path = tmp_path / "artifacts"
225+
226+
mock_require_model_converter_binary.return_value = "model-converter"
227+
mock_model_converter_env.return_value = {"PATH": "/test/bin"}
228+
mock_run.side_effect = backend.subprocess.CalledProcessError(
229+
returncode=1,
230+
cmd=["model-converter"],
231+
output=b"converter stdout",
232+
stderr=b"converter stderr",
233+
)
234+
235+
with pytest.raises(RuntimeError) as exc_info:
236+
vgf_compile(
237+
b"serialized tosa",
238+
["--flag=value with spaces"],
239+
artifact_path=str(artifact_path),
240+
tag_name="delegate_0",
241+
)
242+
243+
copied_tosa_path = os.path.join(
244+
str(artifact_path),
245+
"failed_model_converter_input_delegate_0.tosa",
246+
)
247+
248+
assert os.path.exists(copied_tosa_path)
249+
assert open(copied_tosa_path, "rb").read() == b"serialized tosa"
250+
251+
error = str(exc_info.value)
252+
assert "Vgf compiler failed." in error
253+
assert "Repro command:" in error
254+
assert "model-converter '--flag=value with spaces' -i" in error
255+
assert copied_tosa_path in error
256+
assert " -o " in error
257+
assert "Stderr:\nconverter stderr" in error
258+
assert "Stdout:\nconverter stdout" in error
259+
260+
261+
@mock.patch("executorch.backends.arm.vgf.backend.model_converter_env")
262+
@mock.patch("executorch.backends.arm.vgf.backend.require_model_converter_binary")
263+
@mock.patch("executorch.backends.arm.vgf.backend.subprocess.run")
264+
def test_vgf_compile_failure_includes_temp_repro_command_without_artifact_path(
265+
mock_run,
266+
mock_require_model_converter_binary,
267+
mock_model_converter_env,
268+
):
269+
mock_require_model_converter_binary.return_value = "model-converter"
270+
mock_model_converter_env.return_value = {"PATH": "/test/bin"}
271+
mock_run.side_effect = backend.subprocess.CalledProcessError(
272+
returncode=1,
273+
cmd=["model-converter"],
274+
output=b"converter stdout",
275+
stderr=b"converter stderr",
276+
)
277+
278+
with pytest.raises(RuntimeError) as exc_info:
279+
vgf_compile(
280+
b"serialized tosa",
281+
["--some-flag"],
282+
artifact_path=None,
283+
tag_name="delegate_0",
284+
)
285+
286+
error = str(exc_info.value)
287+
assert "Vgf compiler failed." in error
288+
assert "Repro command:" in error
289+
assert "model-converter --some-flag -i" in error
290+
assert "output_delegate_0.tosa.vgf" in error
291+
assert "failed_model_converter_input_delegate_0.tosa" not in error
292+
assert "Stderr:\nconverter stderr" in error
293+
assert "Stdout:\nconverter stdout" in error

backends/arm/vgf/backend.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import logging
1616
import os # nosec B404 - used alongside subprocess for tool invocation
17+
import shlex
1718
import shutil
1819
import subprocess # nosec B404 - required to drive external converter CLI
1920
import tempfile
@@ -251,6 +252,52 @@ def preprocess(
251252
return PreprocessResult(processed_bytes=binary)
252253

253254

255+
def _format_repro_command(command: List[str]) -> str:
256+
"""Return a shell-safe command string for reproducing converter failures."""
257+
return " ".join(shlex.quote(arg) for arg in command)
258+
259+
260+
def _copy_failure_artifacts(
261+
tosa_path: str,
262+
artifact_path: str | None,
263+
tag_name: str,
264+
) -> str | None:
265+
"""Copy the failing TOSA input to the artifact directory, if configured.
266+
267+
Args:
268+
tosa_path: Temporary TOSA flatbuffer passed to model-converter.
269+
artifact_path: User-configured intermediate artifact directory.
270+
tag_name: Optional delegation tag used to disambiguate artifacts.
271+
272+
Returns:
273+
Path to the copied TOSA file, or None if no artifact path was configured.
274+
275+
"""
276+
if not artifact_path:
277+
return None
278+
279+
os.makedirs(artifact_path, exist_ok=True)
280+
281+
suffix = f"_{tag_name}" if tag_name else ""
282+
failure_tosa_path = os.path.join(
283+
artifact_path,
284+
f"failed_model_converter_input{suffix}.tosa",
285+
)
286+
shutil.copy2(tosa_path, failure_tosa_path)
287+
return failure_tosa_path
288+
289+
290+
def _replace_converter_input_path(
291+
conversion_command: List[str],
292+
input_path: str,
293+
) -> List[str]:
294+
"""Return a converter command that uses a preserved TOSA input path."""
295+
input_flag_index = conversion_command.index("-i")
296+
repro_command = list(conversion_command)
297+
repro_command[input_flag_index + 1] = input_path
298+
return repro_command
299+
300+
254301
def vgf_compile(
255302
tosa_flatbuffer: bytes,
256303
compile_flags: List[str],
@@ -299,11 +346,21 @@ def vgf_compile(
299346
env=model_converter_env(),
300347
)
301348
except subprocess.CalledProcessError as process_error:
302-
conversion_command_str = " ".join(conversion_command)
349+
failure_tosa_path = _copy_failure_artifacts(
350+
tosa_path,
351+
artifact_path,
352+
tag_name,
353+
)
354+
repro_command = (
355+
_replace_converter_input_path(conversion_command, failure_tosa_path)
356+
if failure_tosa_path
357+
else conversion_command
358+
)
303359
raise RuntimeError(
304-
f"Vgf compiler ('{conversion_command_str}') failed with error:\n \
305-
{process_error.stderr.decode()}\n \
306-
Stdout:\n{process_error.stdout.decode()}"
360+
"Vgf compiler failed.\n"
361+
f"Repro command:\n {_format_repro_command(repro_command)}\n"
362+
f"Stderr:\n{process_error.stderr.decode()}\n"
363+
f"Stdout:\n{process_error.stdout.decode()}"
307364
)
308365

309366
if artifact_path:

0 commit comments

Comments
 (0)