Skip to content

Commit 0f02e85

Browse files
authored
Arm backend: Add a repro command when VGF model-converter fails (#20891)
This is retry of this PR, which has been reverted due to a failure after some merging order. #20443 Change-Id: I0edf1504e178d87fce3fd895ace2663161357bfe cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Elena Zhelezina <elena.zhelezina@arm.com>
1 parent 8faf57d commit 0f02e85

2 files changed

Lines changed: 303 additions & 5 deletions

File tree

backends/arm/test/misc/test_vgf_backend.py

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# Copyright 2025-2026 Arm Limited and/or its affiliates.
2+
# Copyright 2026 Arm Limited and/or its affiliates.
23
#
34
# This source code is licensed under the BSD-style license found in the
45
# LICENSE file in the root directory of this source tree.
56

7+
import os
68
from types import SimpleNamespace
79
from typing import cast
10+
from unittest import mock
811

912
import pytest
1013

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

backends/arm/vgf/backend.py

Lines changed: 77 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
@@ -252,6 +253,52 @@ def preprocess(
252253
return PreprocessResult(processed_bytes=binary)
253254

254255

256+
def _format_repro_command(command: List[str]) -> str:
257+
"""Return a shell-safe command string for reproducing converter failures."""
258+
return " ".join(shlex.quote(arg) for arg in command)
259+
260+
261+
def _copy_failure_artifacts(
262+
tosa_path: str,
263+
artifact_path: str | None,
264+
tag_name: str,
265+
) -> str | None:
266+
"""Copy the failing TOSA input to the artifact directory, if configured.
267+
268+
Args:
269+
tosa_path: Temporary TOSA flatbuffer passed to model-converter.
270+
artifact_path: User-configured intermediate artifact directory.
271+
tag_name: Optional delegation tag used to disambiguate artifacts.
272+
273+
Returns:
274+
Path to the copied TOSA file, or None if no artifact path was configured.
275+
276+
"""
277+
if not artifact_path:
278+
return None
279+
280+
os.makedirs(artifact_path, exist_ok=True)
281+
282+
suffix = f"_{tag_name}" if tag_name else ""
283+
failure_tosa_path = os.path.join(
284+
artifact_path,
285+
f"failed_model_converter_input{suffix}.tosa",
286+
)
287+
shutil.copy2(tosa_path, failure_tosa_path)
288+
return failure_tosa_path
289+
290+
291+
def _replace_converter_input_path(
292+
conversion_command: List[str],
293+
input_path: str,
294+
) -> List[str]:
295+
"""Return a converter command that uses a preserved TOSA input path."""
296+
input_flag_index = conversion_command.index("-i")
297+
repro_command = list(conversion_command)
298+
repro_command[input_flag_index + 1] = input_path
299+
return repro_command
300+
301+
255302
def vgf_compile(
256303
tosa_flatbuffer: bytes,
257304
compile_flags: List[str],
@@ -300,11 +347,37 @@ def vgf_compile(
300347
env=model_converter_env(),
301348
)
302349
except subprocess.CalledProcessError as process_error:
303-
conversion_command_str = " ".join(conversion_command)
350+
failure_tosa_path = None
351+
failure_artifact_error = None
352+
353+
try:
354+
failure_tosa_path = _copy_failure_artifacts(
355+
tosa_path,
356+
artifact_path,
357+
tag_name,
358+
)
359+
except Exception as artifact_error:
360+
failure_artifact_error = artifact_error
361+
logger.warning(
362+
"Failed to copy VGF model-converter failure artifacts.",
363+
exc_info=True,
364+
)
365+
repro_command = (
366+
_replace_converter_input_path(conversion_command, failure_tosa_path)
367+
if failure_tosa_path
368+
else conversion_command
369+
)
370+
artifact_note = (
371+
f"Failure artifact copy failed:\n{failure_artifact_error}\n"
372+
if failure_artifact_error
373+
else ""
374+
)
304375
raise RuntimeError(
305-
f"Vgf compiler ('{conversion_command_str}') failed with error:\n \
306-
{process_error.stderr.decode()}\n \
307-
Stdout:\n{process_error.stdout.decode()}"
376+
"Vgf compiler failed.\n"
377+
f"Repro command:\n {_format_repro_command(repro_command)}\n"
378+
f"{artifact_note}"
379+
f"Stderr:\n{process_error.stderr.decode()}\n"
380+
f"Stdout:\n{process_error.stdout.decode()}"
308381
)
309382

310383
if artifact_path:

0 commit comments

Comments
 (0)