Skip to content

Commit c9e4b40

Browse files
liujijCopilot
andauthored
[AMD] Add sd15 support for VitisAI. (microsoft#2359)
## [AMD] Add sd15 support for VitisAI. (microsoft#2359) This PR adds optimized Stable Diffusion model generation support for the Vitis AI Execution Provider. ## Checklist before requesting a review - [x] Add unit tests for this change. - [x] Make sure all tests can pass. - [x] Update documents if necessary. - [x] Lint and apply fixes to your code by running `lintrunner -a` - [x] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. ## (Optional) Issue link --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent afdff9e commit c9e4b40

3 files changed

Lines changed: 384 additions & 0 deletions

File tree

olive/olive_config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,15 @@
682682
"supported_algorithms": [ ],
683683
"supported_quantization_encodings": [ ],
684684
"run_on_target": true
685+
},
686+
"VitisGenerateModelSD": {
687+
"module_path": "olive.passes.onnx.vitis_ai.vitis_generate_model_sd.VitisGenerateModelSD",
688+
"supported_providers": [ "CPUExecutionProvider" ],
689+
"supported_accelerators": [ "cpu" ],
690+
"supported_precisions": [ "int8" ],
691+
"supported_algorithms": [ ],
692+
"supported_quantization_encodings": [ ],
693+
"run_on_target": true
685694
}
686695
},
687696
"extra_dependencies": {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (C) 2026, Advanced Micro Devices, Inc. All rights reserved.
3+
# SPDX-License-Identifier: MIT
4+
# -------------------------------------------------------------------------
5+
6+
"""Olive Pass for Vitis NPU Stable Diffusion submodel generation.
7+
8+
Accepts ONNX input only; run OnnxConversion to produce ONNX input model first,
9+
then this pass runs generate_sd_model to generate NPU-ready models.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import logging
15+
import shutil
16+
from pathlib import Path
17+
18+
from olive.model import ONNXModelHandler
19+
from olive.passes import Pass
20+
from olive.passes.pass_config import BasePassConfig, PassConfigParam
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class VitisGenerateModelSD(Pass):
26+
"""Generate Vitis NPU-ready SD submodel from ONNX input.
27+
28+
Use OnnxConversion to produce ONNX input model.
29+
Optional resolutions to generate NPU-ready models. Default is [512x512].
30+
"""
31+
32+
@classmethod
33+
def _default_config(cls, accelerator_spec):
34+
return {
35+
"model_type": PassConfigParam(
36+
type_=str,
37+
required=True,
38+
description="SD submodel type.",
39+
),
40+
"resolutions": PassConfigParam(
41+
type_=list[str],
42+
default_value=["512x512"],
43+
required=False,
44+
description="List of resolutions (e.g. ['512x512', '1024x1024']) Default is [512x512].",
45+
),
46+
}
47+
48+
def _run_for_config(
49+
self,
50+
model: ONNXModelHandler,
51+
config: BasePassConfig,
52+
output_model_path: str,
53+
) -> ONNXModelHandler:
54+
try:
55+
from model_generate import generate_model
56+
except ImportError as e:
57+
raise ImportError(
58+
"model_generate is required for VitisGenerateModelSD. Please install the model_generate package."
59+
) from e
60+
61+
if not isinstance(model, ONNXModelHandler):
62+
raise TypeError(
63+
f"VitisGenerateModelSD requires ONNXModelHandler (run OnnxConversion first). Got {type(model).__name__}"
64+
)
65+
model_type = config.model_type
66+
67+
output_dir = Path(output_model_path)
68+
if output_dir.suffix == ".onnx":
69+
output_dir = output_dir.parent
70+
output_dir.mkdir(parents=True, exist_ok=True)
71+
72+
logger.info(
73+
"[VitisGenerateModelSD] output_dir=%s, model_type=%s",
74+
output_dir,
75+
model_type,
76+
)
77+
78+
onnx_input_path = self.resolve_onnx_input_path(model)
79+
logger.info("[VitisGenerateModelSD] ONNX input path: %s", onnx_input_path)
80+
81+
resolutions = getattr(config, "resolutions", None)
82+
extra_options = {"model_type": model_type}
83+
if resolutions:
84+
logger.info(
85+
"[VitisGenerateModelSD] Using resolutions: %s",
86+
resolutions,
87+
)
88+
extra_options["resolutions"] = ",".join(resolutions)
89+
90+
generate_model(
91+
mode="sd",
92+
input_model=str(onnx_input_path),
93+
output_dir=str(output_dir),
94+
extra_options=extra_options,
95+
)
96+
97+
self._ensure_model_onnx(output_dir)
98+
99+
return ONNXModelHandler(
100+
model_path=str(output_dir),
101+
onnx_file_name="model.onnx",
102+
)
103+
104+
def resolve_onnx_input_path(self, model: ONNXModelHandler) -> Path:
105+
p = Path(model.model_path)
106+
if p.is_file():
107+
return p
108+
if p.is_dir():
109+
name = getattr(model, "onnx_file_name", None)
110+
if name:
111+
f = p / name
112+
if f.exists():
113+
return f
114+
raise FileNotFoundError(f"Specified onnx_file_name does not exist under {p}: {name}")
115+
116+
default_model_path = p / "model.onnx"
117+
if default_model_path.exists():
118+
return default_model_path
119+
120+
onnx_files = sorted(path for path in p.glob("*.onnx") if path.is_file())
121+
if len(onnx_files) == 1:
122+
return onnx_files[0]
123+
if len(onnx_files) > 1:
124+
candidates = ", ".join(path.name for path in onnx_files)
125+
raise ValueError(
126+
f"Multiple .onnx model files found under {p}: {candidates}. Please specify one using the onnx_file_name argument."
127+
)
128+
else:
129+
raise FileNotFoundError(f"No .onnx file found under {p}")
130+
raise FileNotFoundError(f"Model path does not exist: {p}")
131+
132+
def _ensure_model_onnx(self, output_dir: Path) -> None:
133+
"""Copy actual generate_sd_model output to output_dir/model.onnx if needed."""
134+
model_onnx = output_dir / "model.onnx"
135+
if model_onnx.exists():
136+
return
137+
optimized = output_dir / "optimized.onnx"
138+
dd_replaced = output_dir / "dd" / "replaced.onnx"
139+
if dd_replaced.exists():
140+
shutil.copy2(dd_replaced, model_onnx)
141+
logger.info("[VitisGenerateModelSD] Wrote model.onnx from dd/replaced.onnx")
142+
elif optimized.exists():
143+
shutil.copy2(optimized, model_onnx)
144+
logger.info("[VitisGenerateModelSD] Wrote model.onnx from optimized.onnx")
145+
else:
146+
raise FileNotFoundError(
147+
f"[VitisGenerateModelSD] No optimized.onnx or dd/replaced.onnx found under {output_dir}. Please check the output directory.",
148+
)
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (C) 2026, Advanced Micro Devices, Inc. All rights reserved.
3+
# SPDX-License-Identifier: MIT
4+
# -------------------------------------------------------------------------
5+
6+
from pathlib import Path
7+
from types import SimpleNamespace
8+
from unittest.mock import MagicMock, patch
9+
10+
import pytest
11+
12+
from olive.model import ONNXModelHandler
13+
from olive.passes.olive_pass import create_pass_from_dict
14+
from olive.passes.onnx.vitis_ai.vitis_generate_model_sd import VitisGenerateModelSD
15+
from test.utils import ONNX_MODEL_PATH, get_onnx_model
16+
17+
pytest.importorskip("model_generate", reason="model_generate is not installed; skipping all SD model generation tests")
18+
19+
_PATCH_GEN = "model_generate.generate_model"
20+
21+
22+
def _make_pass(**kwargs):
23+
cfg = {"model_type": "unet", "resolutions": [], **kwargs}
24+
return create_pass_from_dict(VitisGenerateModelSD, cfg, disable_search=True)
25+
26+
27+
def _generate_writes_placeholder(**kwargs):
28+
"""Mock generate_model: leave output Olive's _ensure_model_onnx can satisfy."""
29+
out = Path(kwargs["output_dir"])
30+
out.mkdir(parents=True, exist_ok=True)
31+
(out / "optimized.onnx").write_bytes(b"placeholder")
32+
33+
34+
def test_run_raises_on_missing_model_generate(tmp_path):
35+
p = _make_pass()
36+
with (
37+
patch.dict("sys.modules", {"model_generate": None}),
38+
pytest.raises(
39+
ImportError,
40+
match="model_generate is required for VitisGenerateModelSD",
41+
),
42+
):
43+
p.run(get_onnx_model(), str(tmp_path / "out"))
44+
45+
46+
def test_run_includes_resolutions_in_extra_options(tmp_path):
47+
gen = MagicMock(side_effect=_generate_writes_placeholder)
48+
with patch(_PATCH_GEN, gen):
49+
p = create_pass_from_dict(
50+
VitisGenerateModelSD,
51+
{
52+
"model_type": "unet",
53+
"resolutions": ["512x512", "768x768"],
54+
},
55+
disable_search=True,
56+
)
57+
p.run(get_onnx_model(), str(tmp_path / "sd_out"))
58+
59+
gen.assert_called_once()
60+
kwargs = gen.call_args.kwargs
61+
assert kwargs["mode"] == "sd"
62+
assert kwargs["extra_options"]["model_type"] == "unet"
63+
assert kwargs["extra_options"]["resolutions"] == "512x512,768x768"
64+
65+
66+
def test_run_default_resolutions_passed_when_using_defaults(tmp_path):
67+
gen = MagicMock(side_effect=_generate_writes_placeholder)
68+
with patch(_PATCH_GEN, gen):
69+
p = create_pass_from_dict(
70+
VitisGenerateModelSD,
71+
{"model_type": "unet"},
72+
disable_search=True,
73+
)
74+
p.run(get_onnx_model(), str(tmp_path / "out"))
75+
76+
assert gen.call_args.kwargs["extra_options"].get("resolutions") == "512x512"
77+
78+
79+
def test_run_omits_resolutions_when_empty_list(tmp_path):
80+
gen = MagicMock(side_effect=_generate_writes_placeholder)
81+
with patch(_PATCH_GEN, gen):
82+
p = create_pass_from_dict(
83+
VitisGenerateModelSD,
84+
{"model_type": "unet", "resolutions": []},
85+
disable_search=True,
86+
)
87+
p.run(get_onnx_model(), str(tmp_path / "out"))
88+
89+
assert "resolutions" not in gen.call_args.kwargs["extra_options"]
90+
91+
92+
def test_ensure_model_onnx_copies_optimized(tmp_path):
93+
def write_optimized(**kwargs):
94+
out = Path(kwargs["output_dir"])
95+
(out / "optimized.onnx").write_text("from_optimized", encoding="utf-8")
96+
97+
gen = MagicMock(side_effect=write_optimized)
98+
with patch(_PATCH_GEN, gen):
99+
p = create_pass_from_dict(
100+
VitisGenerateModelSD,
101+
{"model_type": "unet", "resolutions": []},
102+
disable_search=True,
103+
)
104+
p.run(get_onnx_model(), str(tmp_path / "out"))
105+
106+
assert (tmp_path / "out" / "model.onnx").read_text(encoding="utf-8") == "from_optimized"
107+
108+
109+
def test_ensure_model_onnx_prefers_dd_replaced_over_optimized(tmp_path):
110+
def write_both(**kwargs):
111+
out = Path(kwargs["output_dir"])
112+
(out / "optimized.onnx").write_text("from_optimized", encoding="utf-8")
113+
dd = out / "dd"
114+
dd.mkdir(parents=True)
115+
(dd / "replaced.onnx").write_text("from_dd", encoding="utf-8")
116+
117+
gen = MagicMock(side_effect=write_both)
118+
with patch(_PATCH_GEN, gen):
119+
p = create_pass_from_dict(
120+
VitisGenerateModelSD,
121+
{"model_type": "unet", "resolutions": []},
122+
disable_search=True,
123+
)
124+
p.run(get_onnx_model(), str(tmp_path / "out"))
125+
126+
assert (tmp_path / "out" / "model.onnx").read_text(encoding="utf-8") == "from_dd"
127+
128+
129+
def test_ensure_model_onnx_skips_copy_when_model_onnx_exists(tmp_path):
130+
def write_only_original(**kwargs):
131+
out = Path(kwargs["output_dir"])
132+
(out / "model.onnx").write_text("original", encoding="utf-8")
133+
(out / "optimized.onnx").write_text("optimized", encoding="utf-8")
134+
135+
gen = MagicMock(side_effect=write_only_original)
136+
with patch(_PATCH_GEN, gen):
137+
p = create_pass_from_dict(
138+
VitisGenerateModelSD,
139+
{"model_type": "unet", "resolutions": []},
140+
disable_search=True,
141+
)
142+
p.run(get_onnx_model(), str(tmp_path / "out"))
143+
144+
assert (tmp_path / "out" / "model.onnx").read_text(encoding="utf-8") == "original"
145+
146+
147+
def test_ensure_model_onnx_raises_when_no_candidate_files(tmp_path):
148+
gen = MagicMock()
149+
with patch(_PATCH_GEN, gen):
150+
p = create_pass_from_dict(
151+
VitisGenerateModelSD,
152+
{"model_type": "unet", "resolutions": []},
153+
disable_search=True,
154+
)
155+
with pytest.raises(FileNotFoundError, match=r"No optimized\.onnx or dd/replaced\.onnx"):
156+
p.run(get_onnx_model(), str(tmp_path / "out"))
157+
158+
159+
def test_resolve_onnx_input_path_single_file():
160+
p = _make_pass()
161+
h = ONNXModelHandler(model_path=str(ONNX_MODEL_PATH))
162+
assert p.resolve_onnx_input_path(h) == Path(ONNX_MODEL_PATH)
163+
164+
165+
def test_resolve_onnx_input_path_dir_with_model_onnx(tmp_path):
166+
(tmp_path / "model.onnx").write_bytes(b"x")
167+
p = _make_pass()
168+
h = ONNXModelHandler(model_path=str(tmp_path))
169+
assert p.resolve_onnx_input_path(h) == tmp_path / "model.onnx"
170+
171+
172+
def test_resolve_onnx_input_path_dir_with_onnx_file_name(tmp_path):
173+
(tmp_path / "custom.onnx").write_bytes(b"x")
174+
p = _make_pass()
175+
h = ONNXModelHandler(model_path=str(tmp_path), onnx_file_name="custom.onnx")
176+
assert p.resolve_onnx_input_path(h) == tmp_path / "custom.onnx"
177+
178+
179+
def test_resolve_onnx_input_path_dir_onnx_file_name_missing_raises(tmp_path):
180+
p = _make_pass()
181+
h = SimpleNamespace(model_path=str(tmp_path), onnx_file_name="missing.onnx")
182+
with pytest.raises(FileNotFoundError, match="Specified onnx_file_name"):
183+
p.resolve_onnx_input_path(h)
184+
185+
186+
def test_resolve_onnx_input_path_dir_single_unnamed_onnx(tmp_path):
187+
(tmp_path / "only.onnx").write_bytes(b"x")
188+
p = _make_pass()
189+
h = ONNXModelHandler(model_path=str(tmp_path))
190+
assert p.resolve_onnx_input_path(h) == tmp_path / "only.onnx"
191+
192+
193+
def test_resolve_onnx_input_path_dir_multiple_onnx_raises(tmp_path):
194+
(tmp_path / "a.onnx").write_bytes(b"x")
195+
(tmp_path / "b.onnx").write_bytes(b"y")
196+
p = _make_pass()
197+
h = SimpleNamespace(model_path=str(tmp_path))
198+
with pytest.raises(ValueError, match=r"Multiple \.onnx model files found"):
199+
p.resolve_onnx_input_path(h)
200+
201+
202+
def test_resolve_onnx_input_path_dir_no_onnx_raises(tmp_path):
203+
p = _make_pass()
204+
h = SimpleNamespace(model_path=str(tmp_path))
205+
with pytest.raises(FileNotFoundError, match=r"No \.onnx file found"):
206+
p.resolve_onnx_input_path(h)
207+
208+
209+
def test_resolve_onnx_input_path_missing_path_raises(tmp_path):
210+
p = _make_pass()
211+
missing = tmp_path / "nope"
212+
h = SimpleNamespace(model_path=str(missing))
213+
with pytest.raises(FileNotFoundError, match="Model path does not exist"):
214+
p.resolve_onnx_input_path(h)
215+
216+
217+
def test_run_requires_onnx_model_handler(tmp_path):
218+
gen = MagicMock()
219+
with patch(_PATCH_GEN, gen):
220+
p = create_pass_from_dict(
221+
VitisGenerateModelSD,
222+
{"model_type": "unet", "resolutions": []},
223+
disable_search=True,
224+
)
225+
bad = MagicMock()
226+
with pytest.raises(TypeError, match="ONNXModelHandler"):
227+
p.run(bad, str(tmp_path / "out"))

0 commit comments

Comments
 (0)