From 1f0e6616ab08547bc9a380308d73528e79cd02c9 Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Wed, 15 Jul 2026 14:06:35 +0800 Subject: [PATCH 1/3] feat(loader): add pipeline_tag fallback to task detection When architecture-based task detection (Stage 1c) fails because config.architectures contains a generic name like 'Model', fall back to querying the HuggingFace Hub pipeline_tag (Stage 1e) before the last-resort feature-extraction default (Stage 1d). - Add TaskSource.PIPELINE_TAG enum value for provenance tracking - Add Stage 1e in resolve_task() between 1c and 1d: - Checks model_id is not a local path via _is_local_path - Queries HfApi().model_info(model_id).pipeline_tag - Validates against KNOWN_TASKS after normalize_task() - Wrapped in try/except for graceful fallthrough on errors - Add 4 tests covering valid tag, invalid task, API failure, local path Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/winml/modelkit/loader/resolution.py | 19 ++++++ tests/unit/loader/test_detect_task.py | 78 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/winml/modelkit/loader/resolution.py b/src/winml/modelkit/loader/resolution.py index 871c7c817..6ff74896a 100644 --- a/src/winml/modelkit/loader/resolution.py +++ b/src/winml/modelkit/loader/resolution.py @@ -24,6 +24,7 @@ from .task import ( HF_TASK_DEFAULTS, + KNOWN_TASKS, get_default_task_for_model_id, get_supported_tasks, normalize_task, @@ -253,6 +254,7 @@ class TaskSource(str, Enum): SENTINEL_DEFAULT = "sentinel-default" # (model_type, None) sentinel TASKS_MANAGER = "tasks-manager" # Optimum inference (incl. fill-mask upgrade) WRAPPED_LIBRARY = "wrapped-library" # no architectures -> first supported task + PIPELINE_TAG = "pipeline-tag" # Hub pipeline_tag fallback HF_TASK_DEFAULT = "hf-task-default" # last-resort default @@ -572,6 +574,23 @@ def resolve_task( except ValueError: opt_task = None + # 1e. Hub pipeline_tag fallback + if opt_task is None and model_id: + try: + from ..utils.hub_utils import _is_local_path + + if not _is_local_path(model_id): + from huggingface_hub import HfApi + + tag = HfApi().model_info(model_id).pipeline_tag + if tag: + normalized_tag = normalize_task(tag) + if normalized_tag in KNOWN_TASKS: + opt_task = normalized_tag + source = TaskSource.PIPELINE_TAG + except Exception: + pass + # 1d. last-resort default if opt_task is None: opt_task = next(iter(HF_TASK_DEFAULTS)) diff --git a/tests/unit/loader/test_detect_task.py b/tests/unit/loader/test_detect_task.py index e2a39aea6..6eaaf45aa 100644 --- a/tests/unit/loader/test_detect_task.py +++ b/tests/unit/loader/test_detect_task.py @@ -228,3 +228,81 @@ def test_resolve_task_case1_surfaces_modality_aware_task() -> None: r = resolve_task(cfg) assert r.task == "image-feature-extraction" assert r.optimum_task == "feature-extraction" + + +# ============================================================================= +# Stage 1e — Hub pipeline_tag fallback +# ============================================================================= + +_IS_LOCAL = "winml.modelkit.utils.hub_utils._is_local_path" +_HF_API = "huggingface_hub.HfApi" + + +def _fake_model_info(pipeline_tag: str | None): + """Return a mock model_info object with a ``pipeline_tag`` attribute.""" + return type("FakeModelInfo", (), {"pipeline_tag": pipeline_tag})() + + +def test_resolve_task_uses_pipeline_tag_when_architecture_fails() -> None: + """When config.architectures contains a generic name (e.g. 'Model') that + TasksManager cannot resolve, the Hub pipeline_tag is used as fallback.""" + cfg = _FakeConfig("faketype", name_or_path="audeering/wav2vec2-large-robust-24-ft-age-gender") + mock_api = type( + "MockApi", + (), + {"model_info": lambda self, _: _fake_model_info("audio-classification")}, + )() + with ( + patch(_INFER, side_effect=ValueError("unknown arch")), + patch(_IS_LOCAL, return_value=False), + patch(_HF_API, return_value=mock_api), + ): + r = resolve_task(cfg) + assert r.task == "audio-classification" + assert r.source == TaskSource.PIPELINE_TAG + + +def test_resolve_task_pipeline_tag_skips_invalid_task() -> None: + """When pipeline_tag is not a recognized task, falls through to last-resort default.""" + cfg = _FakeConfig("faketype", name_or_path="someone/some-model") + mock_api = type( + "MockApi", + (), + {"model_info": lambda self, _: _fake_model_info("not-a-real-task")}, + )() + with ( + patch(_INFER, side_effect=ValueError("unknown arch")), + patch(_IS_LOCAL, return_value=False), + patch(_HF_API, return_value=mock_api), + ): + r = resolve_task(cfg) + assert r.source == TaskSource.HF_TASK_DEFAULT + + +def test_resolve_task_pipeline_tag_handles_api_failure() -> None: + """When the Hub API call fails (network error, etc.), falls through gracefully.""" + cfg = _FakeConfig("faketype", name_or_path="someone/some-model") + mock_api = type( + "MockApi", + (), + {"model_info": lambda self, _: (_ for _ in ()).throw(ConnectionError("offline"))}, + )() + with ( + patch(_INFER, side_effect=ValueError("unknown arch")), + patch(_IS_LOCAL, return_value=False), + patch(_HF_API, return_value=mock_api), + ): + r = resolve_task(cfg) + assert r.source == TaskSource.HF_TASK_DEFAULT + + +def test_resolve_task_pipeline_tag_skips_local_path() -> None: + """When model_id is a local path, the Hub API is not called.""" + cfg = _FakeConfig("faketype", name_or_path="./local-model") + with ( + patch(_INFER, side_effect=ValueError("unknown arch")), + patch(_IS_LOCAL, return_value=True), + patch(_HF_API, side_effect=AssertionError("must not call HfApi for local paths")), + ): + r = resolve_task(cfg) + assert r.source == TaskSource.HF_TASK_DEFAULT From 03e19dfd64a4a0f8b0fd2df42c88eaa1d58c92fd Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Wed, 15 Jul 2026 14:42:06 +0800 Subject: [PATCH 2/3] fix: add explanatory comment to bare except clause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeQL 'Empty except' finding by documenting that the pass is intentional — network errors, invalid model IDs, and missing pipeline_tag all gracefully fall through to Stage 1d. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/winml/modelkit/loader/resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/winml/modelkit/loader/resolution.py b/src/winml/modelkit/loader/resolution.py index 6ff74896a..a822b8a82 100644 --- a/src/winml/modelkit/loader/resolution.py +++ b/src/winml/modelkit/loader/resolution.py @@ -588,7 +588,7 @@ def resolve_task( if normalized_tag in KNOWN_TASKS: opt_task = normalized_tag source = TaskSource.PIPELINE_TAG - except Exception: + except Exception: # graceful fallthrough: network errors, invalid model_id, etc. pass # 1d. last-resort default From 591e02ee437697313eeb5a1a83e80a152d069a5c Mon Sep 17 00:00:00 2001 From: Hualiang Xie Date: Wed, 15 Jul 2026 17:26:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20address=20review=20=E2=80=94=20?= =?UTF-8?q?centralize=20Hub=20access,=20add=20timeout=20and=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract get_pipeline_tag() helper in hub_utils.py: - Encapsulates _is_local_path check, HfApi call with 10s timeout, and logger.debug on failure (matching existing hub_utils conventions) - Simplify Stage 1e in resolution.py to call get_pipeline_tag() directly - Update resolve_task docstring to mention pipeline-tag stage - Rewrite tests to mock get_pipeline_tag at source, verify call args Addresses review feedback: - Timeout on HfApi().model_info() (was unbounded) - logger.debug breadcrumb instead of bare pass - Centralized Hub access (no more reaching into private _is_local_path) - Stronger test assertion (mock_tag.assert_called_once_with) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/winml/modelkit/loader/resolution.py | 26 ++++++---------- src/winml/modelkit/utils/hub_utils.py | 22 +++++++++++++ tests/unit/loader/test_detect_task.py | 41 ++++++------------------- 3 files changed, 41 insertions(+), 48 deletions(-) diff --git a/src/winml/modelkit/loader/resolution.py b/src/winml/modelkit/loader/resolution.py index a822b8a82..8f9425d80 100644 --- a/src/winml/modelkit/loader/resolution.py +++ b/src/winml/modelkit/loader/resolution.py @@ -442,8 +442,8 @@ def resolve_task( """Resolve a single model's task + class from an HF config. Stages: 0 user override -> 1 detect (override / no-architectures / - TasksManager / default) -> 2 model class -> 3 modality upgrade - (detection path only) -> 4 composite tag. + TasksManager / pipeline-tag / default) -> 2 model class -> 3 modality + upgrade (detection path only) -> 4 composite tag. ``model_type_override`` lets a caller drive resolution with a build variant (e.g. ``qwen3_transformer_only``) without mutating the loaded HF config; when @@ -576,20 +576,14 @@ def resolve_task( # 1e. Hub pipeline_tag fallback if opt_task is None and model_id: - try: - from ..utils.hub_utils import _is_local_path - - if not _is_local_path(model_id): - from huggingface_hub import HfApi - - tag = HfApi().model_info(model_id).pipeline_tag - if tag: - normalized_tag = normalize_task(tag) - if normalized_tag in KNOWN_TASKS: - opt_task = normalized_tag - source = TaskSource.PIPELINE_TAG - except Exception: # graceful fallthrough: network errors, invalid model_id, etc. - pass + from ..utils.hub_utils import get_pipeline_tag + + tag = get_pipeline_tag(model_id) + if tag: + normalized_tag = normalize_task(tag) + if normalized_tag in KNOWN_TASKS: + opt_task = normalized_tag + source = TaskSource.PIPELINE_TAG # 1d. last-resort default if opt_task is None: diff --git a/src/winml/modelkit/utils/hub_utils.py b/src/winml/modelkit/utils/hub_utils.py index d45adf87f..a62876d98 100644 --- a/src/winml/modelkit/utils/hub_utils.py +++ b/src/winml/modelkit/utils/hub_utils.py @@ -144,6 +144,28 @@ def is_hub_model(model_name_or_path: str) -> tuple[bool, dict]: return False, {"type": "local", "path": model_name_or_path} +_PIPELINE_TAG_TIMEOUT = 10 # seconds + + +def get_pipeline_tag(model_id: str) -> str | None: + """Return the Hub ``pipeline_tag`` for *model_id*, or ``None``. + + Lightweight helper that skips the full metadata extraction of + ``is_hub_model``. Returns ``None`` (never raises) when *model_id* is a + local path, the Hub is unreachable, or the model has no tag. + """ + if _is_local_path(model_id): + return None + try: + from huggingface_hub import HfApi + + info = HfApi().model_info(model_id, timeout=_PIPELINE_TAG_TIMEOUT) + return getattr(info, "pipeline_tag", None) + except Exception: + logger.debug("pipeline_tag lookup failed for '%s'", model_id, exc_info=True) + return None + + def inject_hub_metadata(onnx_model: Any, model_name_or_path: str, metadata: dict) -> None: """Inject HuggingFace Hub metadata into ONNX model. diff --git a/tests/unit/loader/test_detect_task.py b/tests/unit/loader/test_detect_task.py index 6eaaf45aa..8b8745488 100644 --- a/tests/unit/loader/test_detect_task.py +++ b/tests/unit/loader/test_detect_task.py @@ -234,28 +234,16 @@ def test_resolve_task_case1_surfaces_modality_aware_task() -> None: # Stage 1e — Hub pipeline_tag fallback # ============================================================================= -_IS_LOCAL = "winml.modelkit.utils.hub_utils._is_local_path" -_HF_API = "huggingface_hub.HfApi" - - -def _fake_model_info(pipeline_tag: str | None): - """Return a mock model_info object with a ``pipeline_tag`` attribute.""" - return type("FakeModelInfo", (), {"pipeline_tag": pipeline_tag})() +_GET_PIPELINE_TAG = "winml.modelkit.utils.hub_utils.get_pipeline_tag" def test_resolve_task_uses_pipeline_tag_when_architecture_fails() -> None: """When config.architectures contains a generic name (e.g. 'Model') that TasksManager cannot resolve, the Hub pipeline_tag is used as fallback.""" cfg = _FakeConfig("faketype", name_or_path="audeering/wav2vec2-large-robust-24-ft-age-gender") - mock_api = type( - "MockApi", - (), - {"model_info": lambda self, _: _fake_model_info("audio-classification")}, - )() with ( patch(_INFER, side_effect=ValueError("unknown arch")), - patch(_IS_LOCAL, return_value=False), - patch(_HF_API, return_value=mock_api), + patch(_GET_PIPELINE_TAG, return_value="audio-classification"), ): r = resolve_task(cfg) assert r.task == "audio-classification" @@ -265,44 +253,33 @@ def test_resolve_task_uses_pipeline_tag_when_architecture_fails() -> None: def test_resolve_task_pipeline_tag_skips_invalid_task() -> None: """When pipeline_tag is not a recognized task, falls through to last-resort default.""" cfg = _FakeConfig("faketype", name_or_path="someone/some-model") - mock_api = type( - "MockApi", - (), - {"model_info": lambda self, _: _fake_model_info("not-a-real-task")}, - )() with ( patch(_INFER, side_effect=ValueError("unknown arch")), - patch(_IS_LOCAL, return_value=False), - patch(_HF_API, return_value=mock_api), + patch(_GET_PIPELINE_TAG, return_value="not-a-real-task"), ): r = resolve_task(cfg) assert r.source == TaskSource.HF_TASK_DEFAULT def test_resolve_task_pipeline_tag_handles_api_failure() -> None: - """When the Hub API call fails (network error, etc.), falls through gracefully.""" + """When the Hub API call fails (network error, etc.), get_pipeline_tag returns None.""" cfg = _FakeConfig("faketype", name_or_path="someone/some-model") - mock_api = type( - "MockApi", - (), - {"model_info": lambda self, _: (_ for _ in ()).throw(ConnectionError("offline"))}, - )() with ( patch(_INFER, side_effect=ValueError("unknown arch")), - patch(_IS_LOCAL, return_value=False), - patch(_HF_API, return_value=mock_api), + patch(_GET_PIPELINE_TAG, return_value=None), ): r = resolve_task(cfg) assert r.source == TaskSource.HF_TASK_DEFAULT def test_resolve_task_pipeline_tag_skips_local_path() -> None: - """When model_id is a local path, the Hub API is not called.""" + """When model_id is a local path, get_pipeline_tag is still called but returns None + (local-path rejection is internal to get_pipeline_tag).""" cfg = _FakeConfig("faketype", name_or_path="./local-model") with ( patch(_INFER, side_effect=ValueError("unknown arch")), - patch(_IS_LOCAL, return_value=True), - patch(_HF_API, side_effect=AssertionError("must not call HfApi for local paths")), + patch(_GET_PIPELINE_TAG, return_value=None) as mock_tag, ): r = resolve_task(cfg) + mock_tag.assert_called_once_with("./local-model") assert r.source == TaskSource.HF_TASK_DEFAULT