Skip to content

Commit de3d719

Browse files
authored
Add genie_overrides to QairtEncapsulation for GenAIConfig customization (microsoft#2469)
Introduce a `genie_overrides` PassConfigParam that deep-merges user-supplied fields into the GenAIConfig before `LLMContainer.export()` bakes them into the Genie DLC. This allows callers to override any GenAIConfig field (engine config, positional encoding, etc.) without modifying `QairtGenAIBuilder` or `QairtPipelinePass`. Nested dicts are merged recursively so only the specified keys are changed; all other values set by the upstream builder pass are preserved. ## Describe your changes Add a `genie_overrides` parameter to `QairtEncapsulation`. When provided, the value is deep-merged into the `GenAIConfig` dict just before `LLMContainer.export()` is called. The merge is recursive: nested dicts are merged key-by-key so callers only need to specify the fields they want to change. A helper `_deep_merge` utility handles the recursion and is unit-tested directly. Integration tests cover round-trip behavior with nested overrides and verify that unrelated config fields are left untouched. ## 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:** `QairtEncapsulation` now accepts `genie_overrides`, `backend_extensions_overrides`, and `engine_config_overrides` parameters that allow users to override the behavior of underlying Genie components when building Genie DLCs. ## (Optional) Issue link N/A
1 parent 491cd7f commit de3d719

5 files changed

Lines changed: 810 additions & 369 deletions

File tree

olive/passes/qairt/encapsulation.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# SPDX-License-Identifier: MIT
44
# --------------------------------------------------------------------------
55

6+
import copy
7+
import inspect
68
import logging
79
import os
810
from pathlib import Path
@@ -24,6 +26,35 @@
2426
MAX_GENIE_CONTEXT_LENGTH = 4096
2527

2628

29+
def _deep_merge(base: dict, overrides: dict) -> dict:
30+
"""Recursively merge *overrides* into *base*, returning a new dict.
31+
32+
Nested dicts are merged rather than replaced. Lists of dicts are merged
33+
element-wise by index — the override list need not be the same length as
34+
the base list; extra base elements are preserved and extra override
35+
elements are appended. A ``None`` override value deletes the key from
36+
the result. The returned dict shares no references with *base*.
37+
"""
38+
result = copy.deepcopy(base)
39+
for k, v in overrides.items():
40+
if v is None:
41+
result.pop(k, None)
42+
elif k in result and isinstance(result[k], dict) and isinstance(v, dict):
43+
result[k] = _deep_merge(result[k], v)
44+
elif k in result and isinstance(result[k], list) and isinstance(v, list):
45+
merged = []
46+
for i, ov in enumerate(v):
47+
if i < len(result[k]) and isinstance(result[k][i], dict) and isinstance(ov, dict):
48+
merged.append(_deep_merge(result[k][i], ov))
49+
else:
50+
merged.append(copy.deepcopy(ov))
51+
merged.extend(result[k][len(v) :])
52+
result[k] = merged
53+
else:
54+
result[k] = copy.deepcopy(v)
55+
return result
56+
57+
2758
class QairtEncapsulation(Pass):
2859
"""Encapsulates a QAIRT DLC model with an onnx protobuf."""
2960

@@ -49,6 +80,27 @@ def _default_config(cls, accelerator_spec: AcceleratorSpec) -> dict[str, PassCon
4980
required=False,
5081
description="Opset name and version to be added in the generated context model",
5182
),
83+
"genie_overrides": PassConfigParam(
84+
type_=dict,
85+
default_value=None,
86+
required=False,
87+
description="Override GenAIConfig fields before DLC export without modifying the builder pass. "
88+
"Only the specified keys are changed; all other builder defaults are preserved.",
89+
),
90+
"backend_extensions_overrides": PassConfigParam(
91+
type_=dict,
92+
default_value=None,
93+
required=False,
94+
description="Override backend extension settings (context, devices, memory, groupContext) "
95+
"before DLC export. Use the same key names as backend_extensions.json.",
96+
),
97+
"engine_config_overrides": PassConfigParam(
98+
type_=dict,
99+
default_value=None,
100+
required=False,
101+
description="Set engine deployment parameters (e.g. n_threads, cpu_mask) passed to the "
102+
"Genie runtime at export. HTP-specific settings go under a nested 'htp' key.",
103+
),
52104
}
53105

54106
def _run_for_config(
@@ -76,6 +128,52 @@ def _run_for_config(
76128

77129
container: qairt_genai.LLMContainer = qairt_genai.LLMContainer.load(model.model_path)
78130

131+
if config.genie_overrides:
132+
if hasattr(container, "_gen_ai_config"):
133+
gen_ai_cfg = container._gen_ai_config # pylint: disable=protected-access
134+
current = gen_ai_cfg.model_dump(mode="json", by_alias=False, exclude_none=True)
135+
merged = _deep_merge(current, config.genie_overrides)
136+
container._gen_ai_config = gen_ai_cfg.model_validate(merged) # pylint: disable=protected-access
137+
logger.info("Applied genie_overrides to GenAIConfig: %s", list(config.genie_overrides.keys()))
138+
else:
139+
logger.warning("genie_overrides ignored: _gen_ai_config not found in installed qairt version")
140+
141+
if config.backend_extensions_overrides:
142+
if hasattr(container, "_backend_extensions_config"):
143+
container._backend_extensions_config = _deep_merge( # pylint: disable=protected-access
144+
container._backend_extensions_config or {}, # pylint: disable=protected-access
145+
config.backend_extensions_overrides,
146+
)
147+
logger.info(
148+
"Applied backend_extensions_overrides: %s", list(config.backend_extensions_overrides.keys())
149+
)
150+
else:
151+
logger.warning(
152+
"backend_extensions_overrides ignored: _backend_extensions_config not found in installed qairt version"
153+
)
154+
155+
export_kwargs = {"export_format": qairt.ExportFormat.LM_EXECUTOR}
156+
if config.engine_config_overrides:
157+
try:
158+
sig = inspect.signature(container.export).parameters
159+
supports_engine_config = "engine_config" in sig or any(
160+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.values()
161+
)
162+
except (TypeError, ValueError):
163+
supports_engine_config = False
164+
if supports_engine_config:
165+
overrides = dict(config.engine_config_overrides)
166+
htp_overrides = overrides.pop("htp", None)
167+
htp_cfg = qairt_genai.HTPEngineConfig(**(htp_overrides or {})) if htp_overrides is not None else None
168+
engine_cfg = qairt_genai.EngineConfig(**overrides, htp=htp_cfg)
169+
export_kwargs["engine_config"] = engine_cfg
170+
else:
171+
logger.warning(
172+
"engine_config_overrides ignored: installed qairt-dev does not support "
173+
"the engine_config parameter in LLMContainer.export(). "
174+
"Upgrade to a newer version of qairt-dev to enable this parameter."
175+
)
176+
79177
# Input/Output metadata
80178
container.inputs = [("input_ids", TensorProto.INT32, ["batch_size", "sequence_length"])]
81179
container.outputs = [("logits", TensorProto.FLOAT, ["batch_size", 1, "vocab_size"])]
@@ -93,7 +191,7 @@ def _run_for_config(
93191
for name, datatype, shape in container.outputs:
94192
outputs.append(helper.make_tensor_value_info(name, datatype, shape))
95193

96-
container.export(output_model_path, export_format=qairt.ExportFormat.LM_EXECUTOR)
194+
container.export(output_model_path, **export_kwargs)
97195

98196
# Find the .dlc file in the output directory
99197
output_path_obj = Path(output_model_path)
@@ -191,12 +289,12 @@ def create_genai_config(
191289
source_config_path = Path(output_path) / "config.json"
192290

193291
if not source_config_path.exists():
194-
raise ValueError("Cannot create gen_ai_config.json if source model config doesn't exist.")
292+
raise ValueError("Cannot create genai_config.json if source model config doesn't exist.")
195293

196294
generation_config_path = Path(output_path) / "generation_config.json"
197295

198296
if not generation_config_path.exists():
199-
raise ValueError("Cannot create gen_ai_config.json if generation config doesn't exist")
297+
raise ValueError("Cannot create genai_config.json if generation config doesn't exist.")
200298

201299
genai_config = {
202300
"model": {

test/passes/qairt/__init__.py

Whitespace-only changes.

test/passes/qairt/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,32 @@
33
# SPDX-License-Identifier: MIT
44
# --------------------------------------------------------------------------
55

6+
from pathlib import Path
67
from unittest.mock import MagicMock, patch
78

89
import pytest
910

1011

12+
@pytest.fixture(name="mock_container")
13+
def mock_container_fixture():
14+
"""Provide a pre-configured LLMContainer mock with input/output metadata and an export stub.
15+
16+
Tests are responsible for wiring LLMContainer.load.return_value so they can customise
17+
container attributes before the pass runs.
18+
"""
19+
container = MagicMock()
20+
container.inputs = [("input_ids", 7, ["batch_size", "sequence_length"])]
21+
container.outputs = [("logits", 1, ["batch_size", 1, "vocab_size"])]
22+
23+
def mock_export(output_dir, *args, **kwargs):
24+
output_dir_path = Path(output_dir)
25+
output_dir_path.mkdir(parents=True, exist_ok=True)
26+
(output_dir_path / "model.dlc").write_text("dummy dlc content")
27+
28+
container.export.side_effect = mock_export
29+
return container
30+
31+
1132
@pytest.fixture(name="mock_qairt_modules")
1233
def mock_qairt_modules_fixture():
1334
"""Mock the qairt and qairt.gen_ai_api modules.

0 commit comments

Comments
 (0)