Skip to content

Commit 3f0e901

Browse files
authored
Arm backend: Add VGF check environment (pytorch#19911)
The VGF backend provides a preflight helper that can be run before export or runtime execution: ```bash python -m executorch.backends.arm.vgf.check_env --aot python -m executorch.backends.arm.vgf.check_env --runtime python -m executorch.backends.arm.vgf.check_env --host-emulator python -m executorch.backends.arm.vgf.check_env --source-build --build-dir cmake-out ``` Use `--aot` before export. It checks that the TOSA serializer and ML SDK model converter are available and that the converter can be launched. Use `--runtime` when debugging Python runtime availability. It checks whether the ExecuTorch runtime backend registry reports VgfBackend as available. Use `--host-emulator` before host-based emulator runs. It checks runtime availability plus Vulkan SDK and ML emulation layer environment variables. Use `--source-build --build-dir <dir>` when debugging a source build. It checks for VGF runtime build prerequisites such as `libvgf` and CMake options including `EXECUTORCH_BUILD_VGF` and `EXECUTORCH_BUILD_VULKAN`. For CI logs or bug reports, add `--json`: ```bash python -m executorch.backends.arm.vgf.check_env --aot --json ``` cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani --------- Signed-off-by: Elena Zhelezina <elena.zhelezina@arm.com>
1 parent 721e641 commit 3f0e901

10 files changed

Lines changed: 1249 additions & 8 deletions

File tree

backends/arm/TARGETS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ runtime.python_library(
9090
"vgf/_passes/__init__.py",
9191
"vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py",
9292
"vgf/backend.py",
93+
"vgf/check_env.py",
9394
"vgf/compile_spec.py",
9495
"vgf/model_converter.py",
9596
"vgf/partitioner.py",

backends/arm/public_api_manifests/api_manifest_running.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ signature = "VgfCompileSpec.dump_intermediate_artifacts_to(self, output_path: st
128128
kind = "function"
129129
signature = "VgfCompileSpec.set_pass_pipeline_config(self, config: executorch.backends.arm.common.pipeline_config.ArmPassPipelineConfig) -> None"
130130

131+
[python.VgfCompileSpec.validate_environment]
132+
kind = "function"
133+
signature = "VgfCompileSpec.validate_environment(self, build_dir: str | None = None, *, require_runtime_build: bool = False) -> 'VgfEnvironmentReport'"
134+
131135
[python.VgfPartitioner]
132136
kind = "class"
133137
signature = "VgfPartitioner(compile_spec: executorch.backends.arm.vgf.compile_spec.VgfCompileSpec, additional_checks: Optional[Sequence[torch.fx.passes.operator_support.OperatorSupportBase]] = None) -> None"
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from __future__ import annotations
7+
8+
import stat
9+
from pathlib import Path
10+
11+
import executorch.backends.arm.vgf.check_env as check_env
12+
13+
import pytest
14+
from executorch.backends.arm.vgf.compile_spec import VgfCompileSpec
15+
16+
17+
def _make_executable(path: Path, body: str) -> Path:
18+
path.write_text(body, encoding="utf-8")
19+
path.chmod(path.stat().st_mode | stat.S_IXUSR)
20+
return path
21+
22+
23+
def _pass(name: str = "ok") -> check_env.VgfEnvironmentCheck:
24+
return check_env.VgfEnvironmentCheck(name, check_env.STATUS_OK, "ok")
25+
26+
27+
def _fail(name: str = "bad") -> check_env.VgfEnvironmentCheck:
28+
return check_env.VgfEnvironmentCheck(name, check_env.STATUS_FAIL, "bad", "fix it")
29+
30+
31+
def test_aot_environment_uses_only_aot_checks(monkeypatch):
32+
monkeypatch.setattr(check_env, "_check_tosa_serializer", lambda: _pass("tosa"))
33+
monkeypatch.setattr(check_env, "_check_model_converter", lambda: _pass("converter"))
34+
monkeypatch.setattr(
35+
check_env, "_check_model_converter_lib_dir", lambda: _pass("lib-dir")
36+
)
37+
38+
report = check_env.check_vgf_aot_environment()
39+
40+
assert report.mode == "aot"
41+
assert report.ok
42+
assert [check.name for check in report.checks] == [
43+
"tosa",
44+
"converter",
45+
"lib-dir",
46+
]
47+
48+
49+
def test_runtime_environment_uses_runtime_check(monkeypatch):
50+
monkeypatch.setattr(
51+
check_env, "_check_runtime_vgf_backend", lambda: _pass("runtime")
52+
)
53+
54+
report = check_env.check_vgf_runtime_environment()
55+
56+
assert report.mode == "runtime"
57+
assert report.ok
58+
assert [check.name for check in report.checks] == ["runtime"]
59+
60+
61+
def test_host_emulator_environment_checks_runtime_vulkan_and_vkml(monkeypatch):
62+
monkeypatch.setattr(
63+
check_env, "_check_runtime_vgf_backend", lambda: _pass("runtime")
64+
)
65+
monkeypatch.setattr(check_env, "_check_vulkan_sdk", lambda: _pass("vulkan"))
66+
monkeypatch.setattr(check_env, "_check_emulation_layer", lambda: _pass("emulation"))
67+
68+
report = check_env.check_vgf_host_emulator_environment()
69+
70+
assert report.mode == "host-emulator"
71+
assert report.ok
72+
assert [check.name for check in report.checks] == [
73+
"runtime",
74+
"vulkan",
75+
"emulation",
76+
]
77+
78+
79+
def test_source_build_environment_checks_vgf_lib_and_cmake(monkeypatch):
80+
captured = {}
81+
82+
def fake_cmake(build_dir, require_runtime_build):
83+
captured["build_dir"] = build_dir
84+
captured["require_runtime_build"] = require_runtime_build
85+
return _pass("cmake")
86+
87+
monkeypatch.setattr(check_env, "_check_vgf_library_path", lambda: _pass("libvgf"))
88+
monkeypatch.setattr(check_env, "_check_cmake_build_flags", fake_cmake)
89+
90+
report = check_env.check_vgf_source_build_environment(build_dir="cmake-out-vkml")
91+
92+
assert report.mode == "source-build"
93+
assert report.ok
94+
assert [check.name for check in report.checks] == ["libvgf", "cmake"]
95+
assert captured == {
96+
"build_dir": "cmake-out-vkml",
97+
"require_runtime_build": True,
98+
}
99+
100+
101+
def test_is_vgf_aot_available(monkeypatch):
102+
monkeypatch.setattr(
103+
check_env,
104+
"check_vgf_aot_environment",
105+
lambda: check_env.VgfEnvironmentReport([_pass()], mode="aot"),
106+
)
107+
108+
assert check_env.is_vgf_aot_available()
109+
110+
111+
def test_is_vgf_runtime_available(monkeypatch):
112+
monkeypatch.setattr(
113+
check_env,
114+
"check_vgf_runtime_environment",
115+
lambda: check_env.VgfEnvironmentReport([_pass()], mode="runtime"),
116+
)
117+
118+
assert check_env.is_vgf_runtime_available()
119+
120+
121+
def test_model_converter_check_fails_when_missing(monkeypatch):
122+
monkeypatch.setattr(check_env, "find_model_converter_binary", lambda: None)
123+
124+
result = check_env._check_model_converter()
125+
126+
assert result.status == check_env.STATUS_FAIL
127+
assert "model-converter" in result.detail
128+
assert result.action is not None
129+
130+
131+
def test_model_converter_check_reports_version(monkeypatch, tmp_path):
132+
converter = _make_executable(
133+
tmp_path / "model-converter",
134+
"#!/usr/bin/env python3\n"
135+
"import sys\n"
136+
"if '--version' in sys.argv:\n"
137+
" print('model-converter 0.9.0')\n"
138+
" raise SystemExit(0)\n"
139+
"raise SystemExit(1)\n",
140+
)
141+
monkeypatch.setattr(
142+
check_env, "find_model_converter_binary", lambda: str(converter)
143+
)
144+
145+
result = check_env._check_model_converter()
146+
147+
assert result.status == check_env.STATUS_OK
148+
assert str(converter) in result.detail
149+
assert "0.9.0" in result.detail
150+
151+
152+
def test_model_converter_lib_dir_fails_when_invalid(monkeypatch, tmp_path):
153+
missing = tmp_path / "missing"
154+
monkeypatch.setenv("MODEL_CONVERTER_LIB_DIR", str(missing))
155+
156+
result = check_env._check_model_converter_lib_dir()
157+
158+
assert result.status == check_env.STATUS_FAIL
159+
assert str(missing) in result.detail
160+
161+
162+
def test_find_existing_lib_finds_libvgf(tmp_path):
163+
lib_dir = tmp_path / "lib"
164+
lib_dir.mkdir()
165+
libvgf = lib_dir / "libvgf.a"
166+
libvgf.write_bytes(b"fake")
167+
168+
found = check_env._find_existing_lib([lib_dir], ("libvgf.a",))
169+
170+
assert found == [libvgf]
171+
172+
173+
def test_runtime_backend_check_passes_when_vgf_registered(monkeypatch):
174+
class BackendRegistry:
175+
registered_backend_names = [check_env.VGF_BACKEND_NAME]
176+
177+
def is_available(self, backend_name):
178+
return backend_name == check_env.VGF_BACKEND_NAME
179+
180+
class Runtime:
181+
backend_registry = BackendRegistry()
182+
183+
monkeypatch.setattr(check_env, "_load_runtime", lambda: Runtime())
184+
185+
result = check_env._check_runtime_vgf_backend()
186+
187+
assert result.status == check_env.STATUS_OK
188+
assert check_env.VGF_BACKEND_NAME in result.detail
189+
190+
191+
def test_runtime_backend_check_fails_when_vgf_not_registered(monkeypatch):
192+
class BackendRegistry:
193+
registered_backend_names = ["XnnpackBackend"]
194+
195+
def is_available(self, backend_name):
196+
return False
197+
198+
class Runtime:
199+
backend_registry = BackendRegistry()
200+
201+
monkeypatch.setattr(check_env, "_load_runtime", lambda: Runtime())
202+
203+
result = check_env._check_runtime_vgf_backend()
204+
205+
assert result.status == check_env.STATUS_FAIL
206+
assert check_env.VGF_BACKEND_NAME in result.detail
207+
assert "XnnpackBackend" in result.detail
208+
209+
210+
def test_cmake_build_flags_pass(tmp_path):
211+
(tmp_path / "CMakeCache.txt").write_text(
212+
"EXECUTORCH_BUILD_VGF:BOOL=ON\n" "EXECUTORCH_BUILD_VULKAN:BOOL=TRUE\n",
213+
encoding="utf-8",
214+
)
215+
216+
result = check_env._check_cmake_build_flags(
217+
build_dir=tmp_path,
218+
require_runtime_build=True,
219+
)
220+
221+
assert result.status == check_env.STATUS_OK
222+
assert "EXECUTORCH_BUILD_VGF=ON" in result.detail
223+
assert "EXECUTORCH_BUILD_VULKAN=TRUE" in result.detail
224+
225+
226+
def test_cmake_build_flags_fail_when_vgf_disabled(tmp_path):
227+
(tmp_path / "CMakeCache.txt").write_text(
228+
"EXECUTORCH_BUILD_VGF:BOOL=OFF\n" "EXECUTORCH_BUILD_VULKAN:BOOL=ON\n",
229+
encoding="utf-8",
230+
)
231+
232+
result = check_env._check_cmake_build_flags(
233+
build_dir=tmp_path,
234+
require_runtime_build=True,
235+
)
236+
237+
assert result.status == check_env.STATUS_FAIL
238+
assert "EXECUTORCH_BUILD_VGF" in result.detail
239+
assert result.action is not None
240+
assert "-DEXECUTORCH_BUILD_VGF=ON" in result.action
241+
242+
243+
def test_cmake_build_flags_warn_when_runtime_build_not_required(tmp_path):
244+
result = check_env._check_cmake_build_flags(
245+
build_dir=None,
246+
require_runtime_build=False,
247+
search_roots=[tmp_path],
248+
)
249+
250+
assert result.status == check_env.STATUS_WARN
251+
252+
253+
def test_report_raise_for_errors():
254+
report = check_env.VgfEnvironmentReport([_fail()])
255+
256+
with pytest.raises(RuntimeError, match="bad"):
257+
report.raise_for_errors()
258+
259+
260+
def test_compile_spec_validate_environment_delegates_to_aot(monkeypatch):
261+
class DummyReport:
262+
def __init__(self):
263+
self.raise_called = False
264+
265+
def raise_for_errors(self):
266+
self.raise_called = True
267+
268+
report = DummyReport()
269+
monkeypatch.setattr(check_env, "check_vgf_aot_environment", lambda: report)
270+
271+
result = VgfCompileSpec().validate_environment()
272+
273+
assert result is report
274+
assert report.raise_called
275+
276+
277+
def test_compile_spec_validate_environment_can_run_source_build(monkeypatch):
278+
class DummyReport:
279+
def __init__(self):
280+
self.raise_called = False
281+
282+
def raise_for_errors(self):
283+
self.raise_called = True
284+
285+
captured = {}
286+
report = DummyReport()
287+
288+
def fake_source_build(build_dir):
289+
captured["build_dir"] = build_dir
290+
return report
291+
292+
monkeypatch.setattr(
293+
check_env, "check_vgf_source_build_environment", fake_source_build
294+
)
295+
296+
result = VgfCompileSpec().validate_environment(
297+
build_dir="cmake-out-vkml",
298+
require_runtime_build=True,
299+
)
300+
301+
assert result is report
302+
assert report.raise_called
303+
assert captured == {"build_dir": "cmake-out-vkml"}
304+
305+
306+
def test_main_defaults_to_aot(monkeypatch, capsys):
307+
monkeypatch.setattr(
308+
check_env,
309+
"check_vgf_aot_environment",
310+
lambda: check_env.VgfEnvironmentReport([_pass("aot")], mode="aot"),
311+
)
312+
313+
assert check_env.main([]) == 0
314+
assert "aot" in capsys.readouterr().out
315+
316+
317+
def test_main_runtime_mode(monkeypatch, capsys):
318+
monkeypatch.setattr(
319+
check_env,
320+
"check_vgf_runtime_environment",
321+
lambda: check_env.VgfEnvironmentReport([_pass("runtime")], mode="runtime"),
322+
)
323+
324+
assert check_env.main(["--runtime"]) == 0
325+
assert "runtime" in capsys.readouterr().out
326+
327+
328+
def test_main_source_build_mode(monkeypatch, capsys):
329+
monkeypatch.setattr(
330+
check_env,
331+
"check_vgf_source_build_environment",
332+
lambda build_dir: check_env.VgfEnvironmentReport(
333+
[_pass(str(build_dir))], mode="source-build"
334+
),
335+
)
336+
337+
assert check_env.main(["--source-build", "--build-dir", "cmake-out-vkml"]) == 0
338+
assert "source-build" in capsys.readouterr().out
339+
340+
341+
def test_main_rejects_build_dir_without_source_build():
342+
with pytest.raises(SystemExit):
343+
check_env.main(["--build-dir", "cmake-out-vkml"])

backends/arm/test/targets.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def define_arm_tests():
6767
"misc/test_debug_hook.py",
6868
"misc/test_mxfp_linear_ao.py",
6969
"misc/test_post_quant_device_switch.py",
70+
"misc/test_vgf_check_env.py",
7071
"misc/test_vgf_backend.py",
7172
# "misc/test_dim_order.py", (TODO - T238390249)
7273
]

0 commit comments

Comments
 (0)