Skip to content

Commit afdff9e

Browse files
authored
Add QairtPipelinePass: single-pass QAIRT LLM pipeline via YAML recipe (microsoft#2465)
## Describe your changes Add `QairtPipelinePass`, a new Olive pass that runs QAIRT's `LLMPipeline` end-to-end from a YAML recipe, replacing the multi-step `QairtPreparation → QairtGenAIBuilder` workflow with a single configurable pass. - Accepts an `HfModelHandler` and a YAML recipe path; executes the full pipeline (model loading, quantization, compilation) and emits a `QairtModelHandler`. - The input `HfModelHandler` is the authoritative model source — if the recipe also specifies `model_id_or_path` and it conflicts with the handler's path, the pass raises a `ValueError`. - `cache_dir` and `log_level` config params allow Olive-level overrides of the corresponding recipe fields. **Files changed:** - `olive/passes/qairt/pipeline.py` — new `QairtPipelinePass` implementation - `olive/olive_config.json` — registers the pass for `QNNExecutionProvider`/NPU - `test/passes/qairt/test_pipeline_pass.py` — unit tests covering success, model-id conflict, override params, missing recipe, invalid input model, and import error cases ## 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. Release Note: * Add QairtPipeline pass for preparation of GenAI models for QCOM devices
1 parent 5ff3a59 commit afdff9e

3 files changed

Lines changed: 415 additions & 0 deletions

File tree

olive/olive_config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,15 @@
520520
"supported_quantization_encodings": [ ],
521521
"extra_dependencies": [ "qairt-dev" ]
522522
},
523+
"QairtPipelinePass": {
524+
"module_path": "olive.passes.qairt.pipeline.QairtPipelinePass",
525+
"supported_providers": [ "QNNExecutionProvider" ],
526+
"supported_accelerators": [ "npu" ],
527+
"supported_precisions": [ "*" ],
528+
"supported_algorithms": [ ],
529+
"supported_quantization_encodings": [ ],
530+
"extra_dependencies": [ "qairt-dev" ]
531+
},
523532
"QairtPreparation": {
524533
"module_path": "olive.passes.qairt.preparation.QairtPreparation",
525534
"supported_providers": [ "QNNExecutionProvider" ],

olive/passes/qairt/pipeline.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
3+
# SPDX-License-Identifier: MIT
4+
# --------------------------------------------------------------------------
5+
6+
import logging
7+
import shutil
8+
from pathlib import Path
9+
10+
from olive.common.config_utils import ParamCategory
11+
from olive.hardware.accelerator import AcceleratorSpec
12+
from olive.model import HfModelHandler, QairtModelHandler
13+
from olive.passes import Pass
14+
from olive.passes.pass_config import BasePassConfig, PassConfigParam
15+
from olive.passes.qairt.utils import QairtLogLevel
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class QairtPipelinePass(Pass):
21+
"""Run a QairtPipeline from a YAML recipe on a HuggingFace model.
22+
23+
Executes the full LLMPipeline workflow (model loading, quantization, compilation)
24+
defined by the recipe and exports the result as a QairtModelHandler. This pass
25+
is intended to replace the QairtPreparation -> QairtGenAIBuilder workflow.
26+
27+
The input HfModelHandler is the authoritative source for the model identity.
28+
If the recipe also specifies model_id_or_path and it differs from the handler's
29+
path, an error is raised. If the recipe omits model_id_or_path, the handler's
30+
path is used.
31+
"""
32+
33+
@classmethod
34+
def _default_config(cls, accelerator_spec: AcceleratorSpec) -> dict[str, PassConfigParam]:
35+
return {
36+
"recipe": PassConfigParam(
37+
type_=str,
38+
required=True,
39+
category=ParamCategory.PATH,
40+
description="Path to the YAML recipe file that defines the LLM pipeline stages "
41+
"(model loading, quantization, genai_builder, etc.).",
42+
),
43+
"cache_dir": PassConfigParam(
44+
type_=str,
45+
required=False,
46+
default_value=None,
47+
description="Directory for pipeline intermediate artifacts. "
48+
"Overrides the recipe's cache_dir field when set.",
49+
),
50+
"log_level": PassConfigParam(
51+
type_=QairtLogLevel,
52+
required=False,
53+
default_value=None,
54+
description="Log level for underlying QAIRT pipeline components. "
55+
"Valid values: DEBUG, INFO, WARNING, ERROR, TRACE. "
56+
"Overrides the recipe's log_level field when set.",
57+
),
58+
}
59+
60+
@classmethod
61+
def validate_config(
62+
cls,
63+
config: type[BasePassConfig],
64+
accelerator_spec: AcceleratorSpec,
65+
) -> bool:
66+
# Only validates the top-level qairt import. The qairt.experimental.pipeline.*
67+
# sub-modules are not checked here; if they are absent (e.g. older SDK), the
68+
# error surfaces in _run_for_config instead.
69+
try:
70+
import qairt # noqa: F401 # pylint: disable=unused-import
71+
except ImportError as exc:
72+
raise ImportError(
73+
"Failed to import QAIRT SDK - please install olive-ai[qairt] to use QAIRT passes. "
74+
"If already installed, please run `qairt-vm -i` for help troubleshooting issues."
75+
) from exc
76+
77+
return True
78+
79+
def _run_for_config(
80+
self,
81+
model: HfModelHandler,
82+
config: type[BasePassConfig],
83+
output_model_path: str,
84+
) -> QairtModelHandler:
85+
try:
86+
import qairt # noqa: F401 # pylint: disable=unused-import
87+
from qairt.experimental.pipeline.torch.common.recipe import Recipe
88+
from qairt.experimental.pipeline.torch.llm.pipeline import LLMPipeline
89+
except ImportError as exc:
90+
raise ImportError(
91+
"Failed to import QAIRT Pipeline API - please install olive-ai[qairt] to use QAIRT passes. "
92+
"If already installed, please run `qairt-vm -i` for help troubleshooting issues."
93+
) from exc
94+
95+
if not isinstance(model, HfModelHandler):
96+
raise ValueError(f"QairtPipelinePass requires HfModelHandler as input, got {type(model).__name__}")
97+
98+
recipe_path = Path(config.recipe).resolve()
99+
if not recipe_path.exists():
100+
raise ValueError(f"Recipe file not found at: {recipe_path}")
101+
102+
recipe_data = dict(Recipe.from_file(recipe_path))
103+
104+
recipe_model_id = recipe_data.get("model_id_or_path")
105+
if recipe_model_id and recipe_model_id != model.model_path:
106+
raise ValueError(
107+
f"Conflict between recipe model_id_or_path '{recipe_model_id}' and input model "
108+
f"path '{model.model_path}'. Remove model_id_or_path from the recipe or ensure "
109+
"it matches the input model path."
110+
)
111+
112+
if config.cache_dir is not None:
113+
recipe_data["cache_dir"] = config.cache_dir
114+
if config.log_level is not None:
115+
recipe_data["log_level"] = config.log_level
116+
117+
pipe = LLMPipeline.from_pretrained(model.model_path, recipe=recipe_data)
118+
pipe.construct()
119+
120+
Path(output_model_path).mkdir(parents=True, exist_ok=True)
121+
pipe.export(output_model_path)
122+
123+
# QairtEncapsulation needs config.json and generation_config.json to generate
124+
# genai_config.json. Resolve the local HF cache path (model.model_path may be a
125+
# HuggingFace repo ID rather than a local directory) and copy if not already present.
126+
try:
127+
from huggingface_hub import snapshot_download
128+
129+
local_model_path = snapshot_download(
130+
model.model_path,
131+
local_files_only=True,
132+
ignore_patterns=["*.pt", "*.bin", "*.safetensors"],
133+
)
134+
except Exception as e:
135+
logger.warning(
136+
"Failed to resolve local HF cache for '%s': %s. File copy will be skipped.",
137+
model.model_path,
138+
e,
139+
)
140+
local_model_path = model.model_path
141+
142+
for fname in ("config.json", "generation_config.json"):
143+
src = Path(local_model_path) / fname
144+
dst = Path(output_model_path) / fname
145+
if src.exists() and not dst.exists():
146+
shutil.copy2(src, dst)
147+
148+
# The pipeline exports chat_template files into a chat_template/ subdirectory.
149+
# QairtEncapsulation expects these as flat files in the model root.
150+
chat_template_dir = Path(output_model_path) / "chat_template"
151+
for fname in ("chat_template.jinja", "tokenizer_config.json"):
152+
src = chat_template_dir / fname
153+
dst = Path(output_model_path) / fname
154+
if src.exists() and not dst.exists():
155+
shutil.copy2(src, dst)
156+
157+
return QairtModelHandler(model_path=output_model_path)

0 commit comments

Comments
 (0)