Skip to content

Commit 3de717f

Browse files
authored
Fix eager attention selection during model loading (#1375)
## Summary - select the configured Transformers attention implementation before `from_pretrained` constructs attention modules - thread export compatibility through both build and direct export paths - cover loader and build propagation while keeping export test doubles compatible ## Motivation DistilBERT exports could instantiate `DistilBertSdpaAttention` and only switch the config to eager immediately before ONNX export. This left the module implementation and config inconsistent, causing integer attention masks to reach SDPA and fail with: ``` Expected attn_mask dtype to be bool or float or to match query dtype ``` ## Validation - `uv run pytest tests/unit/loader/test_load_hf_model.py tests/unit/build/test_hf.py tests/unit/export/test_htp_exporter_attention_compat.py tests/unit/commands/test_config_value_priority.py tests/unit/commands/test_export.py -q --tb=short` - 175 tests passed - retried all 6 affected QNN GPU models: the attention-mask dtype error was eliminated in all 6; 5 completed perf and accuracy successfully, while one progressed to a separate duplicate `metadata_props` ONNX validation failure
1 parent 8e281e2 commit 3de717f

9 files changed

Lines changed: 142 additions & 9 deletions

File tree

scripts/e2e_eval/run_eval.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,11 +2902,11 @@ def parse_args() -> argparse.Namespace:
29022902
"--priority",
29032903
nargs="+",
29042904
choices=["P0", "P1", "P2", "P3"],
2905-
default=["P0", "P1", "P2"],
2905+
default=["P0", "P1", "P2", "P3"],
29062906
metavar="{P0,P1,P2,P3}",
29072907
help=(
29082908
"Filter by priority. Pass one or more, e.g. --priority P0 P1. "
2909-
"Default: P0 P1 P2 (P3 excluded from default runs)."
2909+
"Default: P0 P1 P2 P3."
29102910
),
29112911
)
29122912
parser.add_argument("--model-type", help="Filter by model_type")

src/winml/modelkit/build/hf.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ def _load_model(
350350
path (PR #719 dedup pattern).
351351
"""
352352
task = config.loader.task
353+
attn_implementation = (
354+
config.export.compatibility.transformers_attention if config.export else None
355+
)
353356

354357
if random_init:
355358
from transformers import AutoConfig
@@ -402,7 +405,10 @@ def _load_model(
402405

403406
model_label = model_id or config.loader.model_type
404407
logger.info("Creating random-weight model: %s (from %s)", model_class.__name__, model_label)
405-
return model_class.from_config(hf_config)
408+
model_kwargs: dict[str, Any] = {}
409+
if attn_implementation is not None:
410+
model_kwargs["attn_implementation"] = attn_implementation
411+
return model_class.from_config(hf_config, **model_kwargs)
406412

407413
if model_id is not None:
408414
from ..loader import load_hf_model
@@ -415,6 +421,7 @@ def _load_model(
415421
trust_remote_code=effective_trust,
416422
hf_config=hf_config,
417423
model_type=model_type,
424+
attn_implementation=attn_implementation,
418425
)
419426
return pytorch_model
420427

src/winml/modelkit/commands/export.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,11 @@ def _run_component_export(component_task: str | None, out_path: Path) -> None:
464464
raise click.ClickException(f"Configuration error: {e}") from e
465465

466466
# Load model with task detection (CLI is the orchestration layer)
467-
pytorch_model, _, detected_task = load_hf_model(model, task=component_task)
467+
pytorch_model, _, detected_task = load_hf_model(
468+
model,
469+
task=component_task,
470+
attn_implementation=cfg.compatibility.transformers_attention,
471+
)
468472
if component_task:
469473
console.print(f"[dim]Task (override): {detected_task}[/dim]")
470474
else:

src/winml/modelkit/export/htp/exporter.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ def export(
213213
raise ValueError("Either 'model' or 'model_name_or_path' must be provided.")
214214
from ...loader import load_hf_model
215215

216-
model, _, _ = load_hf_model(model_name_or_path)
216+
model, _, _ = load_hf_model(
217+
model_name_or_path,
218+
attn_implementation=export_config.compatibility.transformers_attention,
219+
)
217220

218221
# Step 1: Model Preparation
219222
model.eval()

src/winml/modelkit/loader/hf.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def load_hf_model(
147147
model_type: str | None = None,
148148
*,
149149
torch_dtype: Any | None = None,
150+
attn_implementation: str | None = None,
150151
) -> tuple[nn.Module, PretrainedConfig, str]:
151152
"""Load, detect task, and prepare HuggingFace model.
152153
@@ -176,6 +177,8 @@ def load_hf_model(
176177
pattern as ``resolve_loader_config(hf_config=...)`` from PR #719.
177178
torch_dtype: Optional dtype policy forwarded to ``from_pretrained``.
178179
Pass ``"auto"`` to preserve the checkpoint's stored dtype.
180+
attn_implementation: Optional Transformers attention implementation
181+
forwarded to ``from_pretrained`` before attention modules are created.
179182
180183
Returns:
181184
Tuple of (model, hf_config, task)
@@ -290,6 +293,8 @@ def load_hf_model(
290293
}
291294
if torch_dtype is not None:
292295
load_kwargs["torch_dtype"] = torch_dtype
296+
if attn_implementation is not None:
297+
load_kwargs["attn_implementation"] = attn_implementation
293298
model = loader_cls.from_pretrained(model_name_or_path, **load_kwargs)
294299

295300
# [5] Export Preparation

tests/unit/build/test_hf.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,59 @@ def test_pretrained_load_threads_model_class(self, sample_config) -> None:
379379
m_load.assert_called_once()
380380
assert m_load.call_args.kwargs["model_class"] == "AutoModelForImageClassification"
381381

382+
def test_pretrained_load_threads_attention_compatibility(self, sample_config) -> None:
383+
"""Attention compatibility is selected before model construction."""
384+
from winml.modelkit.build.hf import _load_model
385+
from winml.modelkit.export.policy import ExportCompatibilityConfig
386+
387+
sample_config.export.compatibility = ExportCompatibilityConfig(
388+
transformers_attention="eager"
389+
)
390+
with patch("winml.modelkit.loader.load_hf_model") as m_load:
391+
m_load.return_value = (MagicMock(), MagicMock(), "image-classification")
392+
_load_model(sample_config, "test-model", trust_remote_code=False)
393+
394+
m_load.assert_called_once()
395+
assert m_load.call_args.kwargs["attn_implementation"] == "eager"
396+
397+
def test_random_init_selects_eager_attention_before_construction(self) -> None:
398+
"""Random-init builds construct the requested attention implementation."""
399+
from transformers import DistilBertConfig
400+
401+
from winml.modelkit.build.hf import _load_model
402+
from winml.modelkit.config import WinMLBuildConfig
403+
404+
config = WinMLBuildConfig.from_dict(
405+
{
406+
"loader": {
407+
"task": "text-classification",
408+
"model_class": "AutoModelForSequenceClassification",
409+
},
410+
"export": {
411+
"compatibility": {"transformers_attention": "eager"},
412+
},
413+
"optim": {},
414+
"quant": None,
415+
"compile": None,
416+
}
417+
)
418+
hf_config = DistilBertConfig(n_layers=1, dim=32, hidden_dim=64, n_heads=4)
419+
420+
model = _load_model(
421+
config,
422+
model_id=None,
423+
trust_remote_code=False,
424+
random_init=True,
425+
hf_config=hf_config,
426+
)
427+
428+
attention_classes = {
429+
type(module).__name__
430+
for module in model.modules()
431+
if "Attention" in type(module).__name__
432+
}
433+
assert attention_classes == {"MultiHeadSelfAttention"}
434+
382435
def test_pre_loaded_model_skips_load(
383436
self, tmp_path: Path, sample_config, mock_pipeline
384437
) -> None:

tests/unit/commands/test_export.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,7 @@ def test_composite_exports_one_onnx_per_component(
10901090
):
10911091
# Echo the per-component task back as the detected task so it flows
10921092
# through to export_onnx and can be verified per sub-model.
1093-
mock_load.side_effect = lambda _model, task=None: (MagicMock(), None, task)
1093+
mock_load.side_effect = lambda _model, task=None, **_kwargs: (MagicMock(), None, task)
10941094
mock_resolve_cfg.return_value = (
10951095
WinMLExportConfig(),
10961096
WinMLLoaderConfig(task="text-generation"),
@@ -1247,7 +1247,7 @@ def fake_export_onnx(**kwargs):
12471247
patch("winml.modelkit.export.resolve_export_config") as mock_resolve_cfg,
12481248
patch("winml.modelkit.export.export_pytorch", side_effect=fake_export_onnx),
12491249
):
1250-
mock_load.side_effect = lambda _model, task=None: (MagicMock(), None, task)
1250+
mock_load.side_effect = lambda _model, task=None, **_kwargs: (MagicMock(), None, task)
12511251
mock_resolve_cfg.return_value = (
12521252
WinMLExportConfig(),
12531253
WinMLLoaderConfig(task="text-generation"),
@@ -1295,7 +1295,7 @@ def test_submodel_exports_only_requested_component(
12951295
patch("winml.modelkit.loader.load_hf_model") as mock_load,
12961296
patch("winml.modelkit.export.resolve_export_config") as mock_resolve_cfg,
12971297
):
1298-
mock_load.side_effect = lambda _model, task=None: (MagicMock(), None, task)
1298+
mock_load.side_effect = lambda _model, task=None, **_kwargs: (MagicMock(), None, task)
12991299
mock_resolve_cfg.return_value = (
13001300
WinMLExportConfig(),
13011301
WinMLLoaderConfig(task="text-generation"),
@@ -1412,7 +1412,7 @@ def test_submodel_allows_input_specs(
14121412
patch("winml.modelkit.loader.load_hf_model") as mock_load,
14131413
patch("winml.modelkit.export.resolve_export_config") as mock_resolve_cfg,
14141414
):
1415-
mock_load.side_effect = lambda _model, task=None: (MagicMock(), None, task)
1415+
mock_load.side_effect = lambda _model, task=None, **_kwargs: (MagicMock(), None, task)
14161416
mock_resolve_cfg.return_value = (
14171417
WinMLExportConfig(),
14181418
WinMLLoaderConfig(task="text-generation"),

tests/unit/export/test_htp_exporter_attention_compat.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
from typing import TYPE_CHECKING
10+
from unittest.mock import patch
1011

1112
import torch
1213
import torch.nn as nn
@@ -143,6 +144,28 @@ def _export_config(*, eager_attention: bool) -> WinMLExportConfig:
143144
)
144145

145146

147+
def test_htp_exporter_auto_load_threads_attention_compatibility(tmp_path: Path) -> None:
148+
export_config = _export_config(eager_attention=True)
149+
150+
with patch("winml.modelkit.loader.load_hf_model") as mock_load:
151+
mock_load.side_effect = RuntimeError("stop after model loading")
152+
try:
153+
HTPExporter().export(
154+
output_path=str(tmp_path / "model.onnx"),
155+
export_config=export_config,
156+
model_name_or_path="fake/model",
157+
)
158+
except RuntimeError as exc:
159+
assert str(exc) == "stop after model loading"
160+
else:
161+
raise AssertionError("Expected the loader sentinel to stop export")
162+
163+
mock_load.assert_called_once_with(
164+
"fake/model",
165+
attn_implementation="eager",
166+
)
167+
168+
146169
def test_htp_exporter_uses_eager_attention_when_policy_requests_it(
147170
monkeypatch: pytest.MonkeyPatch,
148171
tmp_path: Path,

tests/unit/loader/test_load_hf_model.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,44 @@ def test_dtype_is_forwarded_to_task_resolved_class(self, monkeypatch):
233233
torch_dtype="auto",
234234
)
235235

236+
def test_attention_implementation_is_forwarded_before_model_construction(self, monkeypatch):
237+
"""Export compatibility selects attention while constructing the model."""
238+
from types import SimpleNamespace
239+
from unittest.mock import MagicMock
240+
241+
import winml.modelkit.loader.resolution as resolution_module
242+
243+
task_class = MagicMock()
244+
task_class.__name__ = "TaskModel"
245+
task_class.config_class = None
246+
task_model = MagicMock()
247+
task_class.from_pretrained.return_value = task_model
248+
config = SimpleNamespace(model_type="unit", architectures=["CheckpointModel"])
249+
250+
monkeypatch.setattr(
251+
resolution_module,
252+
"resolve_task",
253+
lambda *_a, **_kw: SimpleNamespace(
254+
task="fill-mask",
255+
model_class=task_class,
256+
),
257+
)
258+
259+
model, _, task = load_hf_model(
260+
"fake/model",
261+
hf_config=config,
262+
attn_implementation="eager",
263+
)
264+
265+
assert model is task_model
266+
assert task == "fill-mask"
267+
task_class.from_pretrained.assert_called_once_with(
268+
"fake/model",
269+
trust_remote_code=False,
270+
config=config,
271+
attn_implementation="eager",
272+
)
273+
236274
def test_bert_tiny_uses_model_specific_default_task(self, monkeypatch):
237275
"""bert-tiny should use model-specific default task when task is omitted."""
238276
from unittest.mock import MagicMock

0 commit comments

Comments
 (0)