Skip to content

feat(loader): add pipeline_tag fallback to task detection#1113

Open
xieofxie wants to merge 3 commits into
mainfrom
xieofxie-pipeline-tag-fallback
Open

feat(loader): add pipeline_tag fallback to task detection#1113
xieofxie wants to merge 3 commits into
mainfrom
xieofxie-pipeline-tag-fallback

Conversation

@xieofxie

Copy link
Copy Markdown
Contributor

Summary

When architecture-based task detection (Stage 1c) fails because config.architectures contains a generic name like 'Model' that doesn't map to any real transformers class, task resolution falls through to the last-resort feature-extraction default. However, HuggingFace Hub knows the correct task via pipeline_tag (e.g. audio-classification for audeering/wav2vec2-large-robust-24-ft-age-gender).

This PR adds a Stage 1e between Stage 1c (TasksManager architecture detection) and Stage 1d (last-resort default) that queries the Hub's pipeline_tag as a fallback.

Changes

src/winml/modelkit/loader/resolution.py

  • Added TaskSource.PIPELINE_TAG = "pipeline-tag" enum value for provenance tracking
  • Added KNOWN_TASKS to imports from .task
  • Added Stage 1e block in resolve_task():
    • Only runs when opt_task is None (architecture detection failed) and model_id exists
    • Checks model_id is not a local path via _is_local_path from hub_utils
    • Queries HfApi().model_info(model_id).pipeline_tag
    • Normalizes the tag and validates against KNOWN_TASKS
    • Wrapped in try/except Exception for graceful fallthrough on network errors or invalid model IDs

tests/unit/loader/test_detect_task.py

Added 4 tests:

  • Valid pipeline_tag is used when architecture detection fails
  • Invalid/unknown pipeline_tag falls through to default
  • API failure (network error) falls through gracefully
  • Local path model_id skips the Hub API call entirely

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>
@xieofxie xieofxie requested a review from a team as a code owner July 15, 2026 06:06
Comment thread src/winml/modelkit/loader/resolution.py Fixed
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>
@xieofxie

Copy link
Copy Markdown
Contributor Author

a follow up of #1094

tested via winml export -m audeering/wav2vec2-large-robust-24-ft-age-gender -o temp/wav2vec2.onnx

@DingmaomaoBJTU DingmaomaoBJTU left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pipeline_tag fallback — the overall shape is right: it only runs on the last-resort path (opt_task is None), guards against local paths, and falls through gracefully. A few things worth addressing before merge, left inline:

  1. resolve_task now does blocking network I/O on a path that used to be purely local — no timeout, no caching, and the docstring's stage list wasn't updated.
  2. The bare except Exception: pass drops all diagnostics.
  3. The local-path negative test can't actually fail, because the AssertionError it raises is swallowed by that same except Exception.

Comment thread src/winml/modelkit/loader/resolution.py
Comment thread src/winml/modelkit/loader/resolution.py Outdated
Comment thread src/winml/modelkit/loader/resolution.py Outdated
Comment thread tests/unit/loader/test_detect_task.py Outdated
…ging

- 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>

@DingmaomaoBJTU DingmaomaoBJTU left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the pipeline_tag fallback — the core idea is sound and the resolve_task dispatch is well covered. A few things inline worth a look: the KNOWN_TASKS gate admits display-only tasks that aren't ONNX-exportable; this adds a Hub network call to the shared resolve_task path (build/eval/inference, not just inspect); the 1e/1d labels read out of execution order; and the local-path test mocks the function under test so it doesn't cover the skip.

One more, not inline: the PR description looks slightly stale — it says the HfApi().model_info(...) call is inlined in resolution.py and lists 2 changed files, but the implementation factors it into a get_pipeline_tag helper in utils/hub_utils.py (3 files). Worth updating so the description matches.

tag = get_pipeline_tag(model_id)
if tag:
normalized_tag = normalize_task(tag)
if normalized_tag in KNOWN_TASKS:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KNOWN_TASKS is the display/validation taxonomy and includes tasks that have no ONNX export path (text-to-image, reinforcement-learning, time-series-forecasting, any-to-any, inpainting, ...). I checked, and sentence-similarity is safe because optimum's map_from_synonym collapses it to feature-extraction inside normalize_task — but those others pass through unchanged. So for a model whose pipeline_tag is one of them and whose Stage 1c failed, we'd now set opt_task to a non-exportable task instead of the old feature-extraction default. For inspect that's arguably more accurate, but on the build/eval path it turns a usable default into a hard failure at export / model-class resolution. Consider gating on the exportable set (e.g. intersect with get_supported_tasks / what TasksManager can resolve) rather than the full KNOWN_TASKS.

opt_task = None

# 1e. Hub pipeline_tag fallback
if opt_task is None and model_id:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This puts a Hub round-trip (model_info) on resolve_task, which is shared by build / eval / inference too, not just inspect — those callers reach here whenever --task isn't passed and arch detection fails. Task resolution has been deliberately offline/local up to now, so worth confirming we want the network reach on all of those paths vs. scoping it to inspect. Offline mode degrades fine (the except returns None) and it's guarded to the fallback, so this is more "is this intended" than a bug. Minor: model_info retries transient errors, so worst-case latency can exceed the 10s timeout.

except ValueError:
opt_task = None

# 1e. Hub pipeline_tag fallback

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this block is labeled 1e but executes before 1d (last-resort default), so the code reads 1c -> 1e -> 1d. The docstring already lists them in execution order ("TasksManager / pipeline-tag / default"); renaming pipeline-tag to 1d and the default to 1e would make the inline labels match.

assert r.source == TaskSource.HF_TASK_DEFAULT


def test_resolve_task_pipeline_tag_skips_local_path() -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test mocks get_pipeline_tag, so it never actually exercises the local-path skip — that logic lives inside get_pipeline_tag, which is patched out here. The assertion only confirms the helper is called with "./local-model" and returns the mocked None, so the name/docstring overclaim what's covered: the real _is_local_path rejection and the except Exception fallthrough have no direct test. Suggest a focused unit test on get_pipeline_tag itself (local path -> None without hitting the network; model_info raising -> None).

return False, {"type": "local", "path": model_name_or_path}


_PIPELINE_TAG_TIMEOUT = 10 # seconds

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it smaller ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants