From 281b8683f42342e6fdd57764b76ba1f39ded2356 Mon Sep 17 00:00:00 2001 From: jinman Date: Wed, 4 Nov 2026 16:22:52 +0900 Subject: [PATCH 1/6] Add opt-in model YAML family inheritance with identity proofs Preserve existing alias and update semantics across 334 configs and 595 variants. Add ordered resolution and CPU preprocessing regression tests without changing shipped model definitions. Co-Authored-By: Claude Haiku 5.5 --- .claude/skills/mblt-vision/SKILL.md | 3 + AGENTS.md | 4 + mblt_vision/README.md | 34 ++++++ mblt_vision/_config.py | 120 ++++++++++++++++++++ mblt_vision/wrapper.py | 62 +---------- pyproject.toml | 1 + tests/test_config_inheritance.py | 164 ++++++++++++++++++++++++++++ 7 files changed, 329 insertions(+), 59 deletions(-) create mode 100644 mblt_vision/_config.py create mode 100644 tests/test_config_inheritance.py diff --git a/.claude/skills/mblt-vision/SKILL.md b/.claude/skills/mblt-vision/SKILL.md index 1b9b9c5..b695725 100644 --- a/.claude/skills/mblt-vision/SKILL.md +++ b/.claude/skills/mblt-vision/SKILL.md @@ -28,6 +28,9 @@ description: >- Use file_cfg.filename for MXQ and derive the same-stem ONNX artifact unless onnx_filename is required. - Every post_cfg declares dataset; resolve output taxonomy from the dataset/task pair. +- For opt-in YAML family inheritance, follow `mblt_vision/README.md`'s configuration + inheritance rules. Keep legacy `update` shallow and run `tests/test_config_inheritance.py` + to prove every existing variant's values and processing order remain identical. - Reuse wrapper.download_hub_artifact for any model that needs more than one Hub artifact rather than duplicating Hub-resolution logic. - Registering a new dataset requires readiness (`_*_ready` + `dataset_ready` diff --git a/AGENTS.md b/AGENTS.md index 5fd0aa6..06a243b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,6 +69,10 @@ The current ownership boundary is deliberate: floors, and rounding differs by a pixel at 224/0.9 and 224/0.95. Several names (ConvNext_Base) resolve in timm yet carry torchvision's transform here, so read the expected value from that model's source.yaml provenance rather than from the name. +- To share processing defaults without changing legacy models, opt new family YAMLs into + `extends: bases/family.yaml#DEFAULT`. See the configuration-inheritance section of + `mblt_vision/README.md` for ordering and merge rules. Keep legacy `update` shallow and + prove every existing variant order-identical with `tests/test_config_inheritance.py`. - Require post_cfg.dataset in every model YAML and resolve output class counts using the dataset/task pair. Do not assume one output taxonomy for every model in a task. - Preserve automatic .mxq/.onnx framework detection and the fail-fast error when a local diff --git a/mblt_vision/README.md b/mblt_vision/README.md index 30bbddc..0187f74 100644 --- a/mblt_vision/README.md +++ b/mblt_vision/README.md @@ -60,6 +60,40 @@ and `list_models()` are the preferred discovery and loading APIs for new code. Top-level model imports remain available for compatibility. `obb` is the sole oriented-bounding-box task name. +## Model configuration inheritance + +Model variants may opt into local family YAMLs with `extends`. This keeps the +artifact, preprocessing and dataset connection together without duplicating a +family's processing defaults. Existing `update: VARIANT` and alias behavior is +unchanged; no shipped model YAML has been migrated by this loader refactor. + +```yaml +DEFAULT: + extends: bases/classifier.yaml#DEFAULT + file_cfg: + repo_id: mobilint/YourPublishedModel + filename: model.mxq + revision: approved-hf-commit + pre_cfg: + Resize: + size: 256 +``` + +The example is a schema illustration, not a downloadable model registration. +The base path is relative to the declaring YAML; omit `#DEFAULT` to select the +same default variant. Family bases belong in `models/bases/`, outside top-level +model discovery. `extends` recursively merges mappings and replaces lists and +scalars. Inherited mapping order is preserved: existing preprocessing operations +stay in place and new operations append. Use a standalone pipeline when operation +order must change. In contrast, `update` retains its historical one-section merge, +so replacing `pre_cfg.Resize` replaces that whole operation's parameters. A variant +cannot combine `update` and `extends`; an `update` may reference another variant +that uses `extends`. References are local only and cycles fail before inference. + +`tests/test_config_inheritance.py` compares every existing YAML variant with the +legacy resolver, including ordered processing mappings and derived ONNX filenames, +and checks exact CPU preprocessing equality for representative model families. + ## Pre-Trained Vision Models This section lists the publicly pre-trained models supported by the vision framework. diff --git a/mblt_vision/_config.py b/mblt_vision/_config.py new file mode 100644 index 0000000..ab2fee2 --- /dev/null +++ b/mblt_vision/_config.py @@ -0,0 +1,120 @@ +"""Local YAML model inheritance, independent of inference backends.""" + +from __future__ import annotations + +import copy +from pathlib import Path +from typing import Any + +import yaml + + +def _merge( + base: dict[str, Any], child: dict[str, Any], *, recursive: bool +) -> dict[str, Any]: + """Merge independent mappings while retaining preprocessing operation order.""" + + result = copy.deepcopy(base) + for key, value in child.items(): + if isinstance(result.get(key), dict) and isinstance(value, dict): + if recursive: + result[key] = _merge(result[key], value, recursive=True) + else: + # Historical `update` merges exactly one section level. + result[key].update(copy.deepcopy(value)) + else: + result[key] = copy.deepcopy(value) + return result + + +def load_model_variant( + config_path: str | Path, variant: str = "DEFAULT" +) -> dict[str, Any]: + """Resolve a local YAML variant, its aliases, and optional family inheritance. + + ``update`` retains the legacy same-file, one-section merge semantics. + ``extends: bases/family.yaml#DEFAULT`` opts into recursive mapping inheritance; + the fragment defaults to DEFAULT and paths are relative to the declaring file. + Lists and scalars replace their base values. Mapping key order is retained, so + inherited preprocessing operations retain their order. New operations append; + replace a complete operation sequence with a standalone config when reordering. + Only local files are read; each result is independently deep-copied. + """ + + documents: dict[Path, dict[str, Any]] = {} + resolving: set[tuple[Path, str]] = set() + + def document(path: Path) -> dict[str, Any]: + if path not in documents: + with path.open(encoding="utf-8") as stream: + parsed = yaml.safe_load(stream) + if not isinstance(parsed, dict): + raise TypeError( + f"Model configuration '{path}' should define a dictionary." + ) + documents[path] = parsed + return documents[path] + + def resolve(path: Path, key: str) -> dict[str, Any]: + identity = (path, key) + if identity in resolving: + raise ValueError( + f"Circular model configuration reference detected for '{path}#{key}'." + ) + resolving.add(identity) + try: + full = document(path) + part = full.get(key) + if part is None: + raise ValueError(f"Model type '{key}' not found in configuration.") + if isinstance(part, str): + if part not in full: + raise ValueError( + f"Model alias '{part}' not found in configuration." + ) + return resolve(path, part) + if not isinstance(part, dict): + raise TypeError( + f"Resolved model configuration for '{key}' is not a dictionary." + ) + child = copy.deepcopy(part) + base_key = child.pop("update", None) + has_extends = "extends" in child + reference = child.pop("extends", None) + if base_key is not None and has_extends: + raise ValueError( + "A model variant cannot combine 'update' and 'extends'." + ) + if base_key is not None: + if not isinstance(base_key, str) or base_key not in full: + raise ValueError( + f"Base configuration '{base_key}' not found for update." + ) + return _merge(resolve(path, base_key), child, recursive=False) + if has_extends: + if not isinstance(reference, str) or not reference: + raise ValueError( + "Model 'extends' must be a nonempty local YAML reference." + ) + filename, separator, base_variant = reference.partition("#") + if ( + not filename + or "://" in filename + or (separator and not base_variant) + ): + raise ValueError( + f"Invalid local model inheritance reference: {reference!r}." + ) + if "#" in base_variant: + raise ValueError( + f"Invalid local model inheritance reference: {reference!r}." + ) + base_path = (path.parent / filename).resolve() + return _merge( + resolve(base_path, base_variant or "DEFAULT"), child, recursive=True + ) + return child + finally: + resolving.remove(identity) + + return resolve(Path(config_path).resolve(), variant) diff --git a/mblt_vision/wrapper.py b/mblt_vision/wrapper.py index a15296f..d451284 100644 --- a/mblt_vision/wrapper.py +++ b/mblt_vision/wrapper.py @@ -16,11 +16,11 @@ import numpy as np import torch -import yaml from huggingface_hub import hf_hub_download from huggingface_hub.errors import EntryNotFoundError - from mblt_npu import MobilintNPUBackend, ONNXBackend, normalize_target_device + +from ._config import load_model_variant from ._model_paths import resolve_framework as _resolve_framework from ._model_paths import split_model_paths as _split_model_paths from ._model_paths import ( @@ -365,63 +365,7 @@ def resolve_model_config( if not config_path.is_file(): config_path = MODEL_CONFIG_DIR / _model_name_aliasing(model_cls) - with config_path.open(encoding="utf-8") as config_file: - full_config = yaml.safe_load(config_file) - if not isinstance(full_config, dict): - raise TypeError( - f"Model configuration '{config_path}' should define a dictionary." - ) - - resolving: set[str] = set() - - def _resolve_variant(variant: str) -> dict[str, Any]: - if variant in resolving: - raise ValueError( - f"Circular model configuration reference detected for '{variant}'." - ) - resolving.add(variant) - try: - model_config_part = full_config.get(variant) - if model_config_part is None: - raise ValueError(f"Model type '{variant}' not found in configuration.") - if isinstance(model_config_part, str): - if model_config_part not in full_config: - raise ValueError( - f"Model alias '{model_config_part}' not found in configuration." - ) - return _resolve_variant(model_config_part) - if not isinstance(model_config_part, dict): - raise TypeError( - f"Resolved model configuration for '{variant}' is not a dictionary." - ) - - resolved = copy.deepcopy(model_config_part) - base_config_key = resolved.pop("update", None) - if base_config_key is None: - return resolved - if ( - not isinstance(base_config_key, str) - or base_config_key not in full_config - ): - raise ValueError( - f"Base configuration '{base_config_key}' not found for update." - ) - - merged_config = _resolve_variant(base_config_key) - for key, value in resolved.items(): - if ( - key in merged_config - and isinstance(merged_config[key], dict) - and isinstance(value, dict) - ): - merged_config[key].update(value) - else: - merged_config[key] = value - return merged_config - finally: - resolving.remove(variant) - - return _normalize_model_artifacts(_resolve_variant(model_type)) + return _normalize_model_artifacts(load_model_variant(config_path, model_type)) class MBLT_Engine: diff --git a/pyproject.toml b/pyproject.toml index 5514221..9850c02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ version = { attr = "mblt_vision.__version__" } "mblt_vision" = [ "py.typed", "models/*.yaml", + "models/bases/*.yaml", "datasets/*.yaml", "datasets/*.txt", ] diff --git a/tests/test_config_inheritance.py b/tests/test_config_inheritance.py new file mode 100644 index 0000000..a36907b --- /dev/null +++ b/tests/test_config_inheritance.py @@ -0,0 +1,164 @@ +"""CPU proofs for legacy resolution and opt-in local YAML inheritance.""" + +from __future__ import annotations + +import copy +from pathlib import Path + +import numpy as np +import pytest +import yaml + +from mblt_vision._config import load_model_variant +from mblt_vision.utils.preprocess import build_preprocess +from mblt_vision.wrapper import MODEL_CONFIG_DIR, resolve_model_config + + +def legacy_resolve(document: dict, variant: str) -> dict: + """Independent pre-refactor resolution, including ONNX artifact derivation.""" + + part = document[variant] + if isinstance(part, str): + return legacy_resolve(document, part) + child = copy.deepcopy(part) + base = child.pop("update", None) + if base is not None: + resolved = legacy_resolve(document, base) + for key, value in child.items(): + if isinstance(resolved.get(key), dict) and isinstance(value, dict): + resolved[key].update(value) + else: + resolved[key] = value + return resolved + return child + + +def ordered(value): + """Deep equality alone misses insertion order, which defines pre-op order.""" + + if isinstance(value, dict): + return [(key, ordered(item)) for key, item in value.items()] + if isinstance(value, list): + return [ordered(item) for item in value] + return value + + +def legacy_config(document: dict, variant: str) -> dict: + config = legacy_resolve(document, variant) + files = config.get("file_cfg", {}) + if files.get("filename") and "onnx_filename" not in files: + files["onnx_filename"] = str(Path(files["filename"]).with_suffix(".onnx")) + return config + + +CONFIG_VARIANTS = [ + (path, variant) + for path in sorted(MODEL_CONFIG_DIR.glob("*.yaml")) + for variant in yaml.safe_load(path.read_text(encoding="utf-8")) +] + + +@pytest.mark.parametrize("path,variant", CONFIG_VARIANTS, ids=lambda value: str(value)) +def test_every_legacy_variant_is_order_identical(path: Path, variant: str) -> None: + expected = legacy_config(yaml.safe_load(path.read_text(encoding="utf-8")), variant) + actual = resolve_model_config(str(path), variant) + assert actual == expected + assert ordered(actual) == ordered(expected) + + +def write_config(path: Path, config: dict) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") + return path + + +def test_recursive_extends_keeps_order_and_independent_results(tmp_path: Path) -> None: + write_config( + tmp_path / "bases/base.yaml", + { + "DEFAULT": { + "pre_cfg": { + "Reader": {"style": "pil", "color_mode": "RGB"}, + "Resize": {"size": [32, 32], "interpolation": "bilinear"}, + }, + "post_cfg": {"nested": {"labels": [1, 2], "other": True}}, + } + }, + ) + child = write_config( + tmp_path / "child.yaml", + { + "DEFAULT": { + "extends": "bases/base.yaml", + "pre_cfg": {"Resize": {"size": [16, 16]}, "SetOrder": {"shape": "HWC"}}, + "post_cfg": {"nested": {"labels": [3]}}, + } + }, + ) + result = load_model_variant(child) + assert list(result["pre_cfg"]) == ["Reader", "Resize", "SetOrder"] + assert result["pre_cfg"]["Resize"] == { + "size": [16, 16], + "interpolation": "bilinear", + } + assert result["post_cfg"]["nested"] == {"labels": [3], "other": True} + result["post_cfg"]["nested"]["labels"].append(4) + assert load_model_variant(child)["post_cfg"]["nested"]["labels"] == [3] + + +def test_update_stays_shallow_with_extends_base(tmp_path: Path) -> None: + write_config( + tmp_path / "base.yaml", + {"DEFAULT": {"pre_cfg": {"Resize": {"size": 32, "interpolation": "bilinear"}}}}, + ) + path = write_config( + tmp_path / "child.yaml", + { + "BASE": {"extends": "base.yaml"}, + "DEFAULT": {"update": "BASE", "pre_cfg": {"Resize": {"size": 16}}}, + }, + ) + assert load_model_variant(path)["pre_cfg"]["Resize"] == {"size": 16} + + +@pytest.mark.parametrize( + "reference", + [None, "", 123, "#DEFAULT", "base.yaml#", "https://host/base.yaml", "a#b#c"], +) +def test_invalid_extends(tmp_path: Path, reference) -> None: + path = write_config(tmp_path / "child.yaml", {"DEFAULT": {"extends": reference}}) + with pytest.raises(ValueError): + load_model_variant(path) + + +def test_cross_file_cycle(tmp_path: Path) -> None: + a = write_config(tmp_path / "a.yaml", {"DEFAULT": {"extends": "b.yaml"}}) + write_config(tmp_path / "b.yaml", {"DEFAULT": {"extends": "./a.yaml"}}) + with pytest.raises(ValueError, match="Circular"): + load_model_variant(a) + + +def test_missing_base_and_conflicting_modes(tmp_path: Path) -> None: + path = write_config( + tmp_path / "child.yaml", {"DEFAULT": {"extends": "absent.yaml"}} + ) + with pytest.raises(FileNotFoundError): + load_model_variant(path) + write_config( + path, {"BASE": {}, "DEFAULT": {"extends": "absent.yaml", "update": "BASE"}} + ) + with pytest.raises(ValueError, match="cannot combine"): + load_model_variant(path) + + +@pytest.mark.parametrize("model", ["ResNet50", "YOLOv8n", "YOLOX-s", "DAMO-YOLO-T"]) +def test_cpu_preprocessing_numeric_identity(model: str) -> None: + path = MODEL_CONFIG_DIR / f"{model}.yaml" + expected = legacy_config( + yaml.safe_load(path.read_text(encoding="utf-8")), "DEFAULT" + ) + actual = resolve_model_config(str(path)) + image = np.random.default_rng(0).integers(0, 256, (71, 103, 3), dtype=np.uint8) + before = build_preprocess(expected["pre_cfg"])(image.copy()) + after = build_preprocess(actual["pre_cfg"])(image.copy()) + np.testing.assert_array_equal(np.asarray(after), np.asarray(before)) From e8fe25c01c7ec9513485dfa164957cc3ab138a8e Mon Sep 17 00:00:00 2001 From: jinman Date: Wed, 4 Nov 2026 16:30:30 +0900 Subject: [PATCH 2/6] Add read-only Hub artifact resolution checks Verify exact runtime board paths using metadata only, preserve local-only restrictions and distinguish live commits from approval provenance. Co-Authored-By: Claude Haiku 5.5 --- .claude/skills/mblt-vision/SKILL.md | 3 + AGENTS.md | 3 + mblt_vision/_hub_validation.py | 98 +++++++++++++++++++++++++++++ tests/TEST.md | 16 +++++ tests/test_hub_validation.py | 97 ++++++++++++++++++++++++++++ 5 files changed, 217 insertions(+) create mode 100644 mblt_vision/_hub_validation.py create mode 100644 tests/test_hub_validation.py diff --git a/.claude/skills/mblt-vision/SKILL.md b/.claude/skills/mblt-vision/SKILL.md index b695725..643c86b 100644 --- a/.claude/skills/mblt-vision/SKILL.md +++ b/.claude/skills/mblt-vision/SKILL.md @@ -28,6 +28,9 @@ description: >- Use file_cfg.filename for MXQ and derive the same-stem ONNX artifact unless onnx_filename is required. - Every post_cfg declares dataset; resolve output taxonomy from the dataset/task pair. +- For Hub linkage changes, run `tests/test_hub_validation.py` as described in `tests/TEST.md`. + The metadata checker follows runtime board paths without downloading weights; live commits + are diagnostics, not substitutes for missing historical approval provenance. - For opt-in YAML family inheritance, follow `mblt_vision/README.md`'s configuration inheritance rules. Keep legacy `update` shallow and run `tests/test_config_inheritance.py` to prove every existing variant's values and processing order remain identical. diff --git a/AGENTS.md b/AGENTS.md index 06a243b..a6f72b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,9 @@ The current ownership boundary is deliberate: onnx_path as compatibility aliases. - Update task-package exports and lazy top-level exports together. Confirm that list_models() discovers every public model class. +- Verify Hub configuration changes with the metadata-only checks in + `tests/test_hub_validation.py` (see `tests/TEST.md`). Use exact runtime board paths; + treat live Hub commits as diagnostics, never as invented approval provenance. - Keep each model YAML's file_cfg, pre_cfg, and post_cfg shape stable. file_cfg.filename is the canonical MXQ Hub artifact; derive the same-stem ONNX filename unless the Hub artifact requires an explicit onnx_filename. diff --git a/mblt_vision/_hub_validation.py b/mblt_vision/_hub_validation.py new file mode 100644 index 0000000..d1f136d --- /dev/null +++ b/mblt_vision/_hub_validation.py @@ -0,0 +1,98 @@ +"""Read-only Hub metadata checks for resolved model artifact configurations.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from huggingface_hub import HfApi +from mblt_npu import normalize_target_device + + +@dataclass(frozen=True) +class HubArtifactCheck: + """A metadata-only resolution result; a live commit is not approval provenance.""" + + repo_id: str + revision: str + commit: str | None + framework: str + path: str + status: str + + +def check_hub_artifacts( + config: dict[str, Any], + *, + target_device: str = "aries-rb", + frameworks: tuple[str, ...] = ("mxq", "onnx"), + api: HfApi | None = None, +) -> tuple[HubArtifactCheck, ...]: + """Check exact runtime Hub paths without downloading model bytes. + + Accept a resolved configuration (``resolve_model_config`` derives the ONNX + filename). MXQ is checked only under the normalized target board; ONNX is + checked at the repository root, matching the engine. Never fall back to a + different board, revision or filename. Local-only configs are reported as + skipped, without contacting the Hub. Hub/API exceptions propagate so missing + access or network failure cannot be mistaken for a missing file. + + The returned live commit is diagnostic, not evidence that a commit was + approved. This function does not modify YAMLs or infer approved revisions. + """ + + if not frameworks or any( + framework not in {"mxq", "onnx"} for framework in frameworks + ): + raise ValueError("frameworks must contain only 'mxq' and/or 'onnx'.") + files = config.get("file_cfg") + if not isinstance(files, dict): + raise TypeError("Resolved model configuration requires a file_cfg mapping.") + repo_id, revision = files.get("repo_id"), files.get("revision") + if ( + not isinstance(repo_id, str) + or not repo_id + or not isinstance(revision, str) + or not revision + ): + raise ValueError( + "Hub verification requires nonempty repo_id and revision strings." + ) + board = normalize_target_device(target_device) + paths = [] + for framework in dict.fromkeys(frameworks): + filename = files.get("filename" if framework == "mxq" else "onnx_filename") + if not isinstance(filename, str) or not filename: + raise ValueError( + f"Resolved configuration requires the {framework} artifact filename." + ) + paths.append( + (framework, f"{board}/{filename}" if framework == "mxq" else filename) + ) + if files.get("local_artifact_only"): + return tuple( + HubArtifactCheck( + repo_id, revision, None, framework, path, "local_artifact_only" + ) + for framework, path in paths + ) + client = HfApi() if api is None else api + info = client.model_info(repo_id=repo_id, revision=revision) + if not isinstance(info.sha, str) or not info.sha: + raise ValueError("Hub model metadata did not return a commit SHA.") + # Pin the file listing to the just-resolved commit: symbolic branches can move + # between API calls, and results must describe a single repository snapshot. + available = set( + client.list_repo_files(repo_id=repo_id, revision=info.sha, repo_type="model") + ) + return tuple( + HubArtifactCheck( + repo_id, + revision, + info.sha, + framework, + path, + "resolved" if path in available else "missing", + ) + for framework, path in paths + ) diff --git a/tests/TEST.md b/tests/TEST.md index ffd156d..bea1fcf 100644 --- a/tests/TEST.md +++ b/tests/TEST.md @@ -35,6 +35,22 @@ After authenticating with Hugging Face Hub, exercise representative ONNX models: pytest tests/test_onnx_classification.py -m requires_network ``` +To check representative Hub configurations without downloading model bytes or +using an inference backend: + +```bash +pytest tests/test_hub_validation.py -m requires_network +``` + +These read-only checks use the exact configured `repo_id` and `revision`, verify +MXQ under the normalized target board and ONNX at the repository root, and pin +metadata listing to the resolved commit to avoid races with moving branches. +Local-only artifacts are skipped without a network request. An observed current +commit is diagnostic metadata, not evidence of a historically approved revision; +missing approval provenance must never be replaced with today's `main` tip. +Offline mocked tests cover missing files, wrong boards, API failures and local-only +restrictions. No YAML revision or model artifact is changed by these checks. + To run MXQ inference, add a configured NPU and the shared runtime options: ```bash diff --git a/tests/test_hub_validation.py b/tests/test_hub_validation.py new file mode 100644 index 0000000..f702c88 --- /dev/null +++ b/tests/test_hub_validation.py @@ -0,0 +1,97 @@ +"""Offline unit tests and explicit metadata-only Hub integration checks.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from mblt_vision._hub_validation import check_hub_artifacts +from mblt_vision.wrapper import resolve_model_config + + +def config(**overrides): + return { + "file_cfg": { + "repo_id": "mobilint/example", + "revision": "approved-commit", + "filename": "model.mxq", + "onnx_filename": "model.onnx", + **overrides, + } + } + + +def api_with_files(files): + api = Mock() + api.model_info.return_value = SimpleNamespace(sha="exact-live-commit") + api.list_repo_files.return_value = files + return api + + +def test_exact_paths_and_commit_snapshot() -> None: + api = api_with_files(["aries-rb/model.mxq", "model.onnx"]) + checks = check_hub_artifacts(config(), api=api) + assert [item.status for item in checks] == ["resolved", "resolved"] + assert [item.path for item in checks] == ["aries-rb/model.mxq", "model.onnx"] + assert all(item.commit == "exact-live-commit" for item in checks) + api.model_info.assert_called_once_with( + repo_id="mobilint/example", revision="approved-commit" + ) + api.list_repo_files.assert_called_once_with( + repo_id="mobilint/example", revision="exact-live-commit", repo_type="model" + ) + + +def test_wrong_board_never_falls_back() -> None: + api = api_with_files(["aries-rb/model.mxq", "model.mxq", "model.onnx"]) + checks = check_hub_artifacts(config(), target_device="regulus-ra", api=api) + assert checks[0].path == "regulus-ra/model.mxq" + assert checks[0].status == "missing" + assert checks[1].status == "resolved" + + +def test_local_artifact_only_does_not_contact_hub() -> None: + api = Mock() + checks = check_hub_artifacts(config(local_artifact_only=True), api=api) + assert all( + item.status == "local_artifact_only" and item.commit is None for item in checks + ) + assert not api.mock_calls + + +def test_api_errors_remain_errors() -> None: + api = Mock() + api.model_info.side_effect = ConnectionError("offline") + with pytest.raises(ConnectionError, match="offline"): + check_hub_artifacts(config(), api=api) + + +@pytest.mark.parametrize("frameworks", [(), ("other",)]) +def test_invalid_frameworks(frameworks) -> None: + with pytest.raises(ValueError, match="frameworks"): + check_hub_artifacts(config(), frameworks=frameworks) + + +@pytest.mark.parametrize( + "override", + [{"repo_id": ""}, {"revision": None}, {"filename": None}, {"onnx_filename": ""}], +) +def test_missing_required_fields(override) -> None: + with pytest.raises(ValueError): + check_hub_artifacts(config(**override), api=Mock()) + + +def test_framework_selection_does_not_require_other_filename() -> None: + api = api_with_files(["model.onnx"]) + checks = check_hub_artifacts(config(filename=None), frameworks=("onnx",), api=api) + assert len(checks) == 1 + assert checks[0].status == "resolved" + + +@pytest.mark.requires_network +@pytest.mark.parametrize("model", ["AlexNet", "ResNet50", "YOLOv8n"]) +def test_representative_hub_metadata_resolves(model: str) -> None: + checks = check_hub_artifacts(resolve_model_config(model)) + assert all(item.status == "resolved" for item in checks), checks From 511b8a36e7dd1116eed5fd40128cc378d4c995b0 Mon Sep 17 00:00:00 2001 From: jinman Date: Wed, 4 Nov 2026 16:38:17 +0900 Subject: [PATCH 3/6] Add published ImageNet classifier family configurations Co-Authored-By: Claude Haiku 5.5 --- .claude/skills/mblt-vision/SKILL.md | 3 + AGENTS.md | 4 + mblt_vision/README.md | 19 +++ mblt_vision/image_classification/__init__.py | 82 ++++++++++--- mblt_vision/models/EfficientViT_B3.yaml | 12 ++ mblt_vision/models/EfficientViT_L1.yaml | 8 ++ mblt_vision/models/EfficientViT_L2.yaml | 8 ++ mblt_vision/models/EfficientViT_L3.yaml | 8 ++ mblt_vision/models/LeViT_128.yaml | 12 ++ mblt_vision/models/LeViT_128S.yaml | 12 ++ mblt_vision/models/LeViT_192.yaml | 12 ++ mblt_vision/models/LeViT_256.yaml | 12 ++ mblt_vision/models/LeViT_384.yaml | 12 ++ .../models/ViT_Base_Patch16_CLIP_224.yaml | 14 +++ .../models/ViT_Base_Patch16_RPN_224.yaml | 14 +++ .../models/ViT_Base_Patch32_CLIP_224.yaml | 14 +++ .../models/ViT_Base_Patch32_CLIP_384.yaml | 18 +++ .../models/ViT_Base_Patch32_CLIP_448.yaml | 18 +++ .../models/ViT_Huge_Patch14_CLIP_224.yaml | 11 ++ .../models/ViT_Large_Patch14_CLIP_224.yaml | 11 ++ .../models/ViT_Medium_Patch16_GAP_256.yaml | 18 +++ .../models/ViT_RelPos_Base_Patch16_224.yaml | 14 +++ .../ViT_RelPos_Base_Patch32_Plus_RPN_256.yaml | 18 +++ .../models/ViT_RelPos_Medium_Patch16_224.yaml | 14 +++ .../ViT_RelPos_Medium_Patch16_RPN_224.yaml | 14 +++ .../models/ViT_RelPos_Small_Patch16_224.yaml | 14 +++ .../ViT_SRelPos_Medium_Patch16_224.yaml | 14 +++ .../models/ViT_SRelPos_Small_Patch16_224.yaml | 14 +++ .../models/bases/imagenet_classifier.yaml | 19 +++ tests/test_config_inheritance.py | 4 +- tests/test_published_classifiers.py | 112 ++++++++++++++++++ 31 files changed, 541 insertions(+), 18 deletions(-) create mode 100644 mblt_vision/models/EfficientViT_B3.yaml create mode 100644 mblt_vision/models/EfficientViT_L1.yaml create mode 100644 mblt_vision/models/EfficientViT_L2.yaml create mode 100644 mblt_vision/models/EfficientViT_L3.yaml create mode 100644 mblt_vision/models/LeViT_128.yaml create mode 100644 mblt_vision/models/LeViT_128S.yaml create mode 100644 mblt_vision/models/LeViT_192.yaml create mode 100644 mblt_vision/models/LeViT_256.yaml create mode 100644 mblt_vision/models/LeViT_384.yaml create mode 100644 mblt_vision/models/ViT_Base_Patch16_CLIP_224.yaml create mode 100644 mblt_vision/models/ViT_Base_Patch16_RPN_224.yaml create mode 100644 mblt_vision/models/ViT_Base_Patch32_CLIP_224.yaml create mode 100644 mblt_vision/models/ViT_Base_Patch32_CLIP_384.yaml create mode 100644 mblt_vision/models/ViT_Base_Patch32_CLIP_448.yaml create mode 100644 mblt_vision/models/ViT_Huge_Patch14_CLIP_224.yaml create mode 100644 mblt_vision/models/ViT_Large_Patch14_CLIP_224.yaml create mode 100644 mblt_vision/models/ViT_Medium_Patch16_GAP_256.yaml create mode 100644 mblt_vision/models/ViT_RelPos_Base_Patch16_224.yaml create mode 100644 mblt_vision/models/ViT_RelPos_Base_Patch32_Plus_RPN_256.yaml create mode 100644 mblt_vision/models/ViT_RelPos_Medium_Patch16_224.yaml create mode 100644 mblt_vision/models/ViT_RelPos_Medium_Patch16_RPN_224.yaml create mode 100644 mblt_vision/models/ViT_RelPos_Small_Patch16_224.yaml create mode 100644 mblt_vision/models/ViT_SRelPos_Medium_Patch16_224.yaml create mode 100644 mblt_vision/models/ViT_SRelPos_Small_Patch16_224.yaml create mode 100644 mblt_vision/models/bases/imagenet_classifier.yaml create mode 100644 tests/test_published_classifiers.py diff --git a/.claude/skills/mblt-vision/SKILL.md b/.claude/skills/mblt-vision/SKILL.md index b695725..2093143 100644 --- a/.claude/skills/mblt-vision/SKILL.md +++ b/.claude/skills/mblt-vision/SKILL.md @@ -28,6 +28,9 @@ description: >- Use file_cfg.filename for MXQ and derive the same-stem ONNX artifact unless onnx_filename is required. - Every post_cfg declares dataset; resolve output taxonomy from the dataset/task pair. +- Additional published EfficientViT/LeViT/ViT classifiers use `bases/imagenet_classifier.yaml`. + Preserve the recorded ops transforms checked by `tests/test_published_classifiers.py`, + including the TF-normalized `ViT_Large_Patch14_CLIP_224` exception. - For opt-in YAML family inheritance, follow `mblt_vision/README.md`'s configuration inheritance rules. Keep legacy `update` shallow and run `tests/test_config_inheritance.py` to prove every existing variant's values and processing order remain identical. diff --git a/AGENTS.md b/AGENTS.md index 06a243b..ac74d0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,6 +69,10 @@ The current ownership boundary is deliberate: floors, and rounding differs by a pixel at 224/0.9 and 224/0.95. Several names (ConvNext_Base) resolve in timm yet carry torchvision's transform here, so read the expected value from that model's source.yaml provenance rather than from the name. +- Published EfficientViT, non-convolutional LeViT and additional ViT classifiers inherit + `bases/imagenet_classifier.yaml`. Their exact recorded preprocessing and dataset connection + are pinned by `tests/test_published_classifiers.py`; preserve TF normalization for + `ViT_Large_Patch14_CLIP_224` rather than inferring normalization from its name. - To share processing defaults without changing legacy models, opt new family YAMLs into `extends: bases/family.yaml#DEFAULT`. See the configuration-inheritance section of `mblt_vision/README.md` for ordering and merge rules. Keep legacy `update` shallow and diff --git a/mblt_vision/README.md b/mblt_vision/README.md index 0187f74..f3dfecd 100644 --- a/mblt_vision/README.md +++ b/mblt_vision/README.md @@ -100,6 +100,25 @@ This section lists the publicly pre-trained models supported by the vision frame ### Image Classification +Additional published ImageNet classifiers are available through the same task +subpackage, top-level lazy exports and `MBLT_Engine`: + +- EfficientViT: `EfficientViT_B3`, `EfficientViT_L1`, `EfficientViT_L2`, `EfficientViT_L3`. +- Non-convolutional LeViT: `LeViT_128`, `LeViT_128S`, `LeViT_192`, `LeViT_256`, `LeViT_384`. +- ViT RPN, relative-position, SRelPos, GAP and CLIP families: discover exact names with + `list_models("image_classification")`. + +These configurations extend `models/bases/imagenet_classifier.yaml` and override +only recorded processing differences. The source is each model's +`models/image_classification//_source/predict_float.input.json` and matching +`distribution.yaml` in mblt-model-ops at `6fdbf471a3bd6ec22035a42c8730fe88a68881e4`. +Input crop/resize and normalization are model-specific: in particular, +`ViT_Large_Patch14_CLIP_224` uses TF normalization despite its CLIP name. +All 24 additions retain the existing `revision: main` convention because their +historical approved artifact HF commits are not recorded; current Hub tips and +upstream source-model commits are not substituted as approval pins. This is a +configuration/CPU processing parity addition, not a new inference accuracy claim. + | Model | Input Size
(H,W,C) | AccTop1
(NPU) | AccTop1
(GPU) | FLOPs (B) | params (M) | Source | Note | | --- | --- | --- | --- | --- | --- | --- | --- | | AlexNet | (224,224,3) | 56.084 | 56.556 | 1.43 | 61.10 | [Link](https://docs.pytorch.org/vision/stable/models/generated/torchvision.models.alexnet.html) | | diff --git a/mblt_vision/image_classification/__init__.py b/mblt_vision/image_classification/__init__.py index 3223c59..085defb 100644 --- a/mblt_vision/image_classification/__init__.py +++ b/mblt_vision/image_classification/__init__.py @@ -5,6 +5,14 @@ from .._compat import create_model_class __all__: list[str] = [ + "VGG11", + "VGG11_BN", + "VGG13", + "VGG13_BN", + "VGG16", + "VGG16_BN", + "VGG19", + "VGG19_BN", "AlexNet", "CAFormer_B36", "CAFormer_M36", @@ -21,10 +29,6 @@ "ConvNeXt_Large", "ConvNeXt_Small", "ConvNeXt_Tiny", - "DeiT_Base_Patch16_224", - "DeiT_Base_Patch16_384", - "DeiT_Small_Patch16_224", - "DeiT_Tiny_Patch16_224", "DeiT3_Base_Patch16_224", "DeiT3_Base_Patch16_384", "DeiT3_Large_Patch16_224", @@ -32,6 +36,10 @@ "DeiT3_Medium_Patch16_224", "DeiT3_Small_Patch16_224", "DeiT3_Small_Patch16_384", + "DeiT_Base_Patch16_224", + "DeiT_Base_Patch16_384", + "DeiT_Small_Patch16_224", + "DeiT_Tiny_Patch16_224", "DenseNet121", "DenseNet161", "DenseNet169", @@ -50,11 +58,20 @@ "EfficientNet_V2_L", "EfficientNet_V2_M", "EfficientNet_V2_S", + "EfficientViT_B3", + "EfficientViT_L1", + "EfficientViT_L2", + "EfficientViT_L3", "FlexiViT_Base", "FlexiViT_Large", "FlexiViT_Small", "GoogLeNet", "Inception_V3", + "LeViT_128", + "LeViT_128S", + "LeViT_192", + "LeViT_256", + "LeViT_384", "LeViT_Conv_128", "LeViT_Conv_128S", "LeViT_Conv_192", @@ -89,14 +106,14 @@ "RepViT_M2", "RepViT_M2_3", "RepViT_M3", + "ResNeXt50_32x4d", + "ResNeXt101_32x8d", + "ResNeXt101_64x4d", "ResNet18", "ResNet34", "ResNet50", "ResNet101", "ResNet152", - "ResNeXt50_32x4d", - "ResNeXt101_32x8d", - "ResNeXt101_64x4d", "ShuffleNet_V2_X0_5", "ShuffleNet_V2_X1_0", "ShuffleNet_V2_X1_5", @@ -106,34 +123,41 @@ "Swin_B", "Swin_S", "Swin_T", - "VGG11", - "VGG11_BN", - "VGG13", - "VGG13_BN", - "VGG16", - "VGG16_BN", - "VGG19", - "VGG19_BN", - "VisFormer_Small", - "VisFormer_Tiny", "ViT_B_16", "ViT_B_32", "ViT_Base_Patch8_224", "ViT_Base_Patch16_224", "ViT_Base_Patch16_384", + "ViT_Base_Patch16_CLIP_224", + "ViT_Base_Patch16_RPN_224", "ViT_Base_Patch32_224", "ViT_Base_Patch32_384", + "ViT_Base_Patch32_CLIP_224", + "ViT_Base_Patch32_CLIP_384", + "ViT_Base_Patch32_CLIP_448", + "ViT_Huge_Patch14_CLIP_224", "ViT_L_16", "ViT_L_32", + "ViT_Large_Patch14_CLIP_224", "ViT_Large_Patch16_224", "ViT_Large_Patch16_384", "ViT_Large_Patch32_384", + "ViT_Medium_Patch16_GAP_256", + "ViT_RelPos_Base_Patch16_224", + "ViT_RelPos_Base_Patch32_Plus_RPN_256", + "ViT_RelPos_Medium_Patch16_224", + "ViT_RelPos_Medium_Patch16_RPN_224", + "ViT_RelPos_Small_Patch16_224", + "ViT_SRelPos_Medium_Patch16_224", + "ViT_SRelPos_Small_Patch16_224", "ViT_Small_Patch16_224", "ViT_Small_Patch16_384", "ViT_Small_Patch32_224", "ViT_Small_Patch32_384", "ViT_Tiny_Patch16_224", "ViT_Tiny_Patch16_384", + "VisFormer_Small", + "VisFormer_Tiny", "Wide_ResNet50_2", "Wide_ResNet101_2", "YOLO11lCls", @@ -158,6 +182,30 @@ "YOLOv8xCls", ] +EfficientViT_B3 = create_model_class("EfficientViT_B3", __name__) +EfficientViT_L1 = create_model_class("EfficientViT_L1", __name__) +EfficientViT_L2 = create_model_class("EfficientViT_L2", __name__) +EfficientViT_L3 = create_model_class("EfficientViT_L3", __name__) +LeViT_128 = create_model_class("LeViT_128", __name__) +LeViT_128S = create_model_class("LeViT_128S", __name__) +LeViT_192 = create_model_class("LeViT_192", __name__) +LeViT_256 = create_model_class("LeViT_256", __name__) +LeViT_384 = create_model_class("LeViT_384", __name__) +ViT_RelPos_Medium_Patch16_RPN_224 = create_model_class("ViT_RelPos_Medium_Patch16_RPN_224", __name__) +ViT_RelPos_Base_Patch32_Plus_RPN_256 = create_model_class("ViT_RelPos_Base_Patch32_Plus_RPN_256", __name__) +ViT_Base_Patch16_RPN_224 = create_model_class("ViT_Base_Patch16_RPN_224", __name__) +ViT_Base_Patch16_CLIP_224 = create_model_class("ViT_Base_Patch16_CLIP_224", __name__) +ViT_Base_Patch32_CLIP_448 = create_model_class("ViT_Base_Patch32_CLIP_448", __name__) +ViT_Medium_Patch16_GAP_256 = create_model_class("ViT_Medium_Patch16_GAP_256", __name__) +ViT_Large_Patch14_CLIP_224 = create_model_class("ViT_Large_Patch14_CLIP_224", __name__) +ViT_Base_Patch32_CLIP_224 = create_model_class("ViT_Base_Patch32_CLIP_224", __name__) +ViT_RelPos_Small_Patch16_224 = create_model_class("ViT_RelPos_Small_Patch16_224", __name__) +ViT_Base_Patch32_CLIP_384 = create_model_class("ViT_Base_Patch32_CLIP_384", __name__) +ViT_SRelPos_Medium_Patch16_224 = create_model_class("ViT_SRelPos_Medium_Patch16_224", __name__) +ViT_Huge_Patch14_CLIP_224 = create_model_class("ViT_Huge_Patch14_CLIP_224", __name__) +ViT_RelPos_Base_Patch16_224 = create_model_class("ViT_RelPos_Base_Patch16_224", __name__) +ViT_RelPos_Medium_Patch16_224 = create_model_class("ViT_RelPos_Medium_Patch16_224", __name__) +ViT_SRelPos_Small_Patch16_224 = create_model_class("ViT_SRelPos_Small_Patch16_224", __name__) AlexNet = create_model_class("AlexNet", __name__) CAFormer_B36 = create_model_class("CAFormer_B36", __name__) CAFormer_M36 = create_model_class("CAFormer_M36", __name__) diff --git a/mblt_vision/models/EfficientViT_B3.yaml b/mblt_vision/models/EfficientViT_B3.yaml new file mode 100644 index 0000000..7f90dff --- /dev/null +++ b/mblt_vision/models/EfficientViT_B3.yaml @@ -0,0 +1,12 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/EfficientViT_B3/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/EfficientViT_B3 + filename: efficientvit_b3.mxq + revision: main + pre_cfg: + Resize: + interpolation: bicubic + size: 235 diff --git a/mblt_vision/models/EfficientViT_L1.yaml b/mblt_vision/models/EfficientViT_L1.yaml new file mode 100644 index 0000000..6dc9ab3 --- /dev/null +++ b/mblt_vision/models/EfficientViT_L1.yaml @@ -0,0 +1,8 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/EfficientViT_L1/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/EfficientViT_L1 + filename: efficientvit_l1.mxq + revision: main diff --git a/mblt_vision/models/EfficientViT_L2.yaml b/mblt_vision/models/EfficientViT_L2.yaml new file mode 100644 index 0000000..f09469d --- /dev/null +++ b/mblt_vision/models/EfficientViT_L2.yaml @@ -0,0 +1,8 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/EfficientViT_L2/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/EfficientViT_L2 + filename: efficientvit_l2.mxq + revision: main diff --git a/mblt_vision/models/EfficientViT_L3.yaml b/mblt_vision/models/EfficientViT_L3.yaml new file mode 100644 index 0000000..20f5c75 --- /dev/null +++ b/mblt_vision/models/EfficientViT_L3.yaml @@ -0,0 +1,8 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/EfficientViT_L3/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/EfficientViT_L3 + filename: efficientvit_l3.mxq + revision: main diff --git a/mblt_vision/models/LeViT_128.yaml b/mblt_vision/models/LeViT_128.yaml new file mode 100644 index 0000000..dd200ef --- /dev/null +++ b/mblt_vision/models/LeViT_128.yaml @@ -0,0 +1,12 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/LeViT_128/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/LeViT_128 + filename: levit_128.mxq + revision: main + pre_cfg: + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/LeViT_128S.yaml b/mblt_vision/models/LeViT_128S.yaml new file mode 100644 index 0000000..5b347aa --- /dev/null +++ b/mblt_vision/models/LeViT_128S.yaml @@ -0,0 +1,12 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/LeViT_128S/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/LeViT_128S + filename: levit_128s.mxq + revision: main + pre_cfg: + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/LeViT_192.yaml b/mblt_vision/models/LeViT_192.yaml new file mode 100644 index 0000000..536a983 --- /dev/null +++ b/mblt_vision/models/LeViT_192.yaml @@ -0,0 +1,12 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/LeViT_192/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/LeViT_192 + filename: levit_192.mxq + revision: main + pre_cfg: + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/LeViT_256.yaml b/mblt_vision/models/LeViT_256.yaml new file mode 100644 index 0000000..b7dfb0a --- /dev/null +++ b/mblt_vision/models/LeViT_256.yaml @@ -0,0 +1,12 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/LeViT_256/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/LeViT_256 + filename: levit_256.mxq + revision: main + pre_cfg: + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/LeViT_384.yaml b/mblt_vision/models/LeViT_384.yaml new file mode 100644 index 0000000..e0502d2 --- /dev/null +++ b/mblt_vision/models/LeViT_384.yaml @@ -0,0 +1,12 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/LeViT_384/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/LeViT_384 + filename: levit_384.mxq + revision: main + pre_cfg: + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_Base_Patch16_CLIP_224.yaml b/mblt_vision/models/ViT_Base_Patch16_CLIP_224.yaml new file mode 100644 index 0000000..7f31c24 --- /dev/null +++ b/mblt_vision/models/ViT_Base_Patch16_CLIP_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Base_Patch16_CLIP_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Base_Patch16_CLIP_224 + filename: vit_base_patch16_clip_224.mxq + revision: main + pre_cfg: + Normalize: + style: openai + Resize: + interpolation: bicubic + size: 235 diff --git a/mblt_vision/models/ViT_Base_Patch16_RPN_224.yaml b/mblt_vision/models/ViT_Base_Patch16_RPN_224.yaml new file mode 100644 index 0000000..013fe25 --- /dev/null +++ b/mblt_vision/models/ViT_Base_Patch16_RPN_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Base_Patch16_RPN_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Base_Patch16_RPN_224 + filename: vit_base_patch16_rpn_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_Base_Patch32_CLIP_224.yaml b/mblt_vision/models/ViT_Base_Patch32_CLIP_224.yaml new file mode 100644 index 0000000..5c6464a --- /dev/null +++ b/mblt_vision/models/ViT_Base_Patch32_CLIP_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Base_Patch32_CLIP_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Base_Patch32_CLIP_224 + filename: vit_base_patch32_clip_224.mxq + revision: main + pre_cfg: + Normalize: + style: openai + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_Base_Patch32_CLIP_384.yaml b/mblt_vision/models/ViT_Base_Patch32_CLIP_384.yaml new file mode 100644 index 0000000..8086fc2 --- /dev/null +++ b/mblt_vision/models/ViT_Base_Patch32_CLIP_384.yaml @@ -0,0 +1,18 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Base_Patch32_CLIP_384/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Base_Patch32_CLIP_384 + filename: vit_base_patch32_clip_384.mxq + revision: main + pre_cfg: + CenterCrop: + size: + - 384 + - 384 + Normalize: + style: openai + Resize: + interpolation: bicubic + size: 384 diff --git a/mblt_vision/models/ViT_Base_Patch32_CLIP_448.yaml b/mblt_vision/models/ViT_Base_Patch32_CLIP_448.yaml new file mode 100644 index 0000000..c29252b --- /dev/null +++ b/mblt_vision/models/ViT_Base_Patch32_CLIP_448.yaml @@ -0,0 +1,18 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Base_Patch32_CLIP_448/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Base_Patch32_CLIP_448 + filename: vit_base_patch32_clip_448.mxq + revision: main + pre_cfg: + CenterCrop: + size: + - 448 + - 448 + Normalize: + style: openai + Resize: + interpolation: bicubic + size: 448 diff --git a/mblt_vision/models/ViT_Huge_Patch14_CLIP_224.yaml b/mblt_vision/models/ViT_Huge_Patch14_CLIP_224.yaml new file mode 100644 index 0000000..8fbe652 --- /dev/null +++ b/mblt_vision/models/ViT_Huge_Patch14_CLIP_224.yaml @@ -0,0 +1,11 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Huge_Patch14_CLIP_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Huge_Patch14_CLIP_224 + filename: vit_huge_patch14_clip_224.mxq + revision: main + pre_cfg: + Normalize: + style: openai diff --git a/mblt_vision/models/ViT_Large_Patch14_CLIP_224.yaml b/mblt_vision/models/ViT_Large_Patch14_CLIP_224.yaml new file mode 100644 index 0000000..d30d8c2 --- /dev/null +++ b/mblt_vision/models/ViT_Large_Patch14_CLIP_224.yaml @@ -0,0 +1,11 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Large_Patch14_CLIP_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Large_Patch14_CLIP_224 + filename: vit_large_patch14_clip_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf diff --git a/mblt_vision/models/ViT_Medium_Patch16_GAP_256.yaml b/mblt_vision/models/ViT_Medium_Patch16_GAP_256.yaml new file mode 100644 index 0000000..d26adc3 --- /dev/null +++ b/mblt_vision/models/ViT_Medium_Patch16_GAP_256.yaml @@ -0,0 +1,18 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_Medium_Patch16_GAP_256/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_Medium_Patch16_GAP_256 + filename: vit_medium_patch16_gap_256.mxq + revision: main + pre_cfg: + CenterCrop: + size: + - 256 + - 256 + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 269 diff --git a/mblt_vision/models/ViT_RelPos_Base_Patch16_224.yaml b/mblt_vision/models/ViT_RelPos_Base_Patch16_224.yaml new file mode 100644 index 0000000..d5d0831 --- /dev/null +++ b/mblt_vision/models/ViT_RelPos_Base_Patch16_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_RelPos_Base_Patch16_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_RelPos_Base_Patch16_224 + filename: vit_relpos_base_patch16_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_RelPos_Base_Patch32_Plus_RPN_256.yaml b/mblt_vision/models/ViT_RelPos_Base_Patch32_Plus_RPN_256.yaml new file mode 100644 index 0000000..2549da1 --- /dev/null +++ b/mblt_vision/models/ViT_RelPos_Base_Patch32_Plus_RPN_256.yaml @@ -0,0 +1,18 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_RelPos_Base_Patch32_Plus_RPN_256/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_RelPos_Base_Patch32_Plus_RPN_256 + filename: vit_relpos_base_patch32_plus_rpn_256.mxq + revision: main + pre_cfg: + CenterCrop: + size: + - 256 + - 256 + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 284 diff --git a/mblt_vision/models/ViT_RelPos_Medium_Patch16_224.yaml b/mblt_vision/models/ViT_RelPos_Medium_Patch16_224.yaml new file mode 100644 index 0000000..033ebef --- /dev/null +++ b/mblt_vision/models/ViT_RelPos_Medium_Patch16_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_RelPos_Medium_Patch16_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_RelPos_Medium_Patch16_224 + filename: vit_relpos_medium_patch16_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_RelPos_Medium_Patch16_RPN_224.yaml b/mblt_vision/models/ViT_RelPos_Medium_Patch16_RPN_224.yaml new file mode 100644 index 0000000..4674b70 --- /dev/null +++ b/mblt_vision/models/ViT_RelPos_Medium_Patch16_RPN_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_RelPos_Medium_Patch16_RPN_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_RelPos_Medium_Patch16_RPN_224 + filename: vit_relpos_medium_patch16_rpn_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_RelPos_Small_Patch16_224.yaml b/mblt_vision/models/ViT_RelPos_Small_Patch16_224.yaml new file mode 100644 index 0000000..ba1f738 --- /dev/null +++ b/mblt_vision/models/ViT_RelPos_Small_Patch16_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_RelPos_Small_Patch16_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_RelPos_Small_Patch16_224 + filename: vit_relpos_small_patch16_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_SRelPos_Medium_Patch16_224.yaml b/mblt_vision/models/ViT_SRelPos_Medium_Patch16_224.yaml new file mode 100644 index 0000000..74e32bf --- /dev/null +++ b/mblt_vision/models/ViT_SRelPos_Medium_Patch16_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_SRelPos_Medium_Patch16_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_SRelPos_Medium_Patch16_224 + filename: vit_srelpos_medium_patch16_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/ViT_SRelPos_Small_Patch16_224.yaml b/mblt_vision/models/ViT_SRelPos_Small_Patch16_224.yaml new file mode 100644 index 0000000..809b7c0 --- /dev/null +++ b/mblt_vision/models/ViT_SRelPos_Small_Patch16_224.yaml @@ -0,0 +1,14 @@ +# Approval HF commit: not recorded. Do not substitute upstream source-model commits. +# Preprocessing: mblt-model-ops models/image_classification/ViT_SRelPos_Small_Patch16_224/_source/predict_float.input.json +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/ViT_SRelPos_Small_Patch16_224 + filename: vit_srelpos_small_patch16_224.mxq + revision: main + pre_cfg: + Normalize: + style: tf + Resize: + interpolation: bicubic + size: 248 diff --git a/mblt_vision/models/bases/imagenet_classifier.yaml b/mblt_vision/models/bases/imagenet_classifier.yaml new file mode 100644 index 0000000..bc4b08b --- /dev/null +++ b/mblt_vision/models/bases/imagenet_classifier.yaml @@ -0,0 +1,19 @@ +# Shared ordered ImageNet preprocessing; model-specific values follow recorded ops inputs. +DEFAULT: + pre_cfg: + Reader: + style: pil + Resize: + size: 224 + interpolation: bicubic + CenterCrop: + size: + - 224 + - 224 + SetOrder: + shape: HWC + Normalize: + style: torch + post_cfg: + task: image_classification + dataset: imagenet diff --git a/tests/test_config_inheritance.py b/tests/test_config_inheritance.py index a36907b..7b561d4 100644 --- a/tests/test_config_inheritance.py +++ b/tests/test_config_inheritance.py @@ -54,7 +54,9 @@ def legacy_config(document: dict, variant: str) -> dict: CONFIG_VARIANTS = [ (path, variant) for path in sorted(MODEL_CONFIG_DIR.glob("*.yaml")) - for variant in yaml.safe_load(path.read_text(encoding="utf-8")) + for variant, definition in yaml.safe_load(path.read_text(encoding="utf-8")).items() + # Only pre-existing alias/update configurations have a legacy counterpart. + if not isinstance(definition, dict) or "extends" not in definition ] diff --git a/tests/test_published_classifiers.py b/tests/test_published_classifiers.py new file mode 100644 index 0000000..27d23b3 --- /dev/null +++ b/tests/test_published_classifiers.py @@ -0,0 +1,112 @@ +"""CPU processing and discovery contracts for additional published classifiers.""" + +from __future__ import annotations + +import numpy as np +import pytest +from PIL import Image + +import mblt_vision +from mblt_vision.utils.preprocess import build_preprocess +from mblt_vision.wrapper import resolve_model_config + +# Model-specific values from the recorded ops predict_float inputs and distribution +# contracts at 6fdbf471a3bd6ec22035a42c8730fe88a68881e4. Keep the surprising TF +# normalization of ViT_Large_Patch14_CLIP_224: names do not define transforms. +SPECS = { + "EfficientViT_B3": (235, 224, "torch"), + "EfficientViT_L1": (224, 224, "torch"), + "EfficientViT_L2": (224, 224, "torch"), + "EfficientViT_L3": (224, 224, "torch"), + "LeViT_128": (248, 224, "torch"), + "LeViT_128S": (248, 224, "torch"), + "LeViT_192": (248, 224, "torch"), + "LeViT_256": (248, 224, "torch"), + "LeViT_384": (248, 224, "torch"), + "ViT_RelPos_Medium_Patch16_RPN_224": (248, 224, "tf"), + "ViT_RelPos_Base_Patch32_Plus_RPN_256": (284, 256, "tf"), + "ViT_Base_Patch16_RPN_224": (248, 224, "tf"), + "ViT_Base_Patch16_CLIP_224": (235, 224, "openai"), + "ViT_Base_Patch32_CLIP_448": (448, 448, "openai"), + "ViT_Medium_Patch16_GAP_256": (269, 256, "tf"), + "ViT_Large_Patch14_CLIP_224": (224, 224, "tf"), + "ViT_Base_Patch32_CLIP_224": (248, 224, "openai"), + "ViT_RelPos_Small_Patch16_224": (248, 224, "tf"), + "ViT_Base_Patch32_CLIP_384": (384, 384, "openai"), + "ViT_SRelPos_Medium_Patch16_224": (248, 224, "tf"), + "ViT_Huge_Patch14_CLIP_224": (224, 224, "openai"), + "ViT_RelPos_Base_Patch16_224": (248, 224, "tf"), + "ViT_RelPos_Medium_Patch16_224": (248, 224, "tf"), + "ViT_SRelPos_Small_Patch16_224": (248, 224, "tf"), +} +STATS = { + "torch": ([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), + "tf": ([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), + "openai": ( + [0.48145466, 0.4578275, 0.40821073], + [0.26862954, 0.26130258, 0.27577711], + ), +} + + +@pytest.mark.parametrize("model", SPECS) +def test_configs_match_recorded_ops_inputs_and_discover(model: str) -> None: + resize, crop, style = SPECS[model] + config = resolve_model_config(model) + assert config["pre_cfg"] == { + "Reader": {"style": "pil"}, + "Resize": {"size": resize, "interpolation": "bicubic"}, + "CenterCrop": {"size": [crop, crop]}, + "SetOrder": {"shape": "HWC"}, + "Normalize": {"style": style}, + } + assert list(config["pre_cfg"]) == [ + "Reader", + "Resize", + "CenterCrop", + "SetOrder", + "Normalize", + ] + assert config["post_cfg"] == {"task": "image_classification", "dataset": "imagenet"} + assert config["file_cfg"]["repo_id"] == f"mobilint/{model}" + assert config["file_cfg"]["revision"] == "main" + assert ( + model in mblt_vision.list_models("image_classification")["image_classification"] + ) + assert getattr(mblt_vision, model) is getattr( + mblt_vision.image_classification, model + ) + + +@pytest.mark.parametrize("model", SPECS) +@pytest.mark.parametrize("shape", [(277, 391), (399, 271), (257, 257)]) +def test_cpu_numeric_parity_with_ops_adapter_math( + model: str, shape: tuple[int, int], tmp_path +) -> None: + resize, crop, style = SPECS[model] + image = Image.fromarray( + np.random.default_rng(0).integers(0, 256, (*shape, 3), dtype=np.uint8) + ) + source = tmp_path / "input.png" + image.save(source) + # Independent equivalent of ops customer_preprocessing_imagenet_torch.py; + # TF/OpenAI adapters use the same resize/crop with their own recorded stats. + width, height = image.size + target = ( + (resize, int(resize * height / width)) + if width <= height + else (int(resize * width / height), resize) + ) + image = image.resize(target, Image.Resampling.BICUBIC) + left, top = round((image.width - crop) / 2), round((image.height - crop) / 2) + reference = ( + np.asarray(image.crop((left, top, left + crop, top + crop)), dtype=np.float32) + / 255 + ) + mean, std = STATS[style] + reference = (reference - np.asarray(mean, dtype=np.float32)) / np.asarray( + std, dtype=np.float32 + ) + actual = build_preprocess(resolve_model_config(model)["pre_cfg"])(str(source)) + assert actual.dtype == np.float32 + np.testing.assert_allclose(actual, reference, rtol=0, atol=5e-7) From b1dbd45926940fca4edec730ca07efe245d2bf8e Mon Sep 17 00:00:00 2001 From: jinman Date: Wed, 4 Nov 2026 16:42:12 +0900 Subject: [PATCH 4/6] Require local ONNX for MXQ-only published classifier repositories Keep existing marker-free configs unchanged and preserve Hub MXQ loading. Skip only marked ONNX in metadata verification, with positive and negative CPU guard tests. Co-Authored-By: Claude Haiku 5.5 --- .claude/skills/mblt-vision/SKILL.md | 4 +- AGENTS.md | 2 + mblt_vision/README.md | 5 ++ mblt_vision/_hub_validation.py | 16 +++- .../models/bases/imagenet_classifier.yaml | 2 + mblt_vision/wrapper.py | 5 ++ tests/test_onnx_local_artifact.py | 76 +++++++++++++++++++ tests/test_published_classifiers.py | 1 + 8 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/test_onnx_local_artifact.py diff --git a/.claude/skills/mblt-vision/SKILL.md b/.claude/skills/mblt-vision/SKILL.md index 623d61b..5de5a20 100644 --- a/.claude/skills/mblt-vision/SKILL.md +++ b/.claude/skills/mblt-vision/SKILL.md @@ -30,7 +30,9 @@ description: >- - Every post_cfg declares dataset; resolve output taxonomy from the dataset/task pair. - Additional published EfficientViT/LeViT/ViT classifiers use `bases/imagenet_classifier.yaml`. Preserve the recorded ops transforms checked by `tests/test_published_classifiers.py`, - including the TF-normalized `ViT_Large_Patch14_CLIP_224` exception. + including the TF-normalized `ViT_Large_Patch14_CLIP_224` exception. Their MXQ-only + repositories use `onnx_local_artifact_only: true`: require trusted local ONNX and + retain automatic MXQ loading; metadata validation skips only the marked framework. - For Hub linkage changes, run `tests/test_hub_validation.py` as described in `tests/TEST.md`. The metadata checker follows runtime board paths without downloading weights; live commits are diagnostics, not substitutes for missing historical approval provenance. diff --git a/AGENTS.md b/AGENTS.md index 05bd139..411570e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,8 @@ The current ownership boundary is deliberate: `bases/imagenet_classifier.yaml`. Their exact recorded preprocessing and dataset connection are pinned by `tests/test_published_classifiers.py`; preserve TF normalization for `ViT_Large_Patch14_CLIP_224` rather than inferring normalization from its name. + These 24 repositories are MXQ-only: their base uses `onnx_local_artifact_only: true`. + Keep automatic MXQ loading enabled, require trusted local ONNX, and test both guard paths. - To share processing defaults without changing legacy models, opt new family YAMLs into `extends: bases/family.yaml#DEFAULT`. See the configuration-inheritance section of `mblt_vision/README.md` for ordering and merge rules. Keep legacy `update` shallow and diff --git a/mblt_vision/README.md b/mblt_vision/README.md index f3dfecd..3e3c8bf 100644 --- a/mblt_vision/README.md +++ b/mblt_vision/README.md @@ -118,6 +118,11 @@ All 24 additions retain the existing `revision: main` convention because their historical approved artifact HF commits are not recorded; current Hub tips and upstream source-model commits are not substituted as approval pins. This is a configuration/CPU processing parity addition, not a new inference accuracy claim. +All 24 Hub repositories expose MXQ only, with no published ONNX file. Their base +sets `file_cfg.onnx_local_artifact_only: true`: MXQ Hub loading remains enabled, +but ONNX requires a trusted local `model_path` or `onnx_path`. Automatic ONNX +loading fails with a specific message, and metadata verification skips only that +framework. Existing configurations without this marker retain their behavior. | Model | Input Size
(H,W,C) | AccTop1
(NPU) | AccTop1
(GPU) | FLOPs (B) | params (M) | Source | Note | | --- | --- | --- | --- | --- | --- | --- | --- | diff --git a/mblt_vision/_hub_validation.py b/mblt_vision/_hub_validation.py index d1f136d..901373b 100644 --- a/mblt_vision/_hub_validation.py +++ b/mblt_vision/_hub_validation.py @@ -69,7 +69,13 @@ def check_hub_artifacts( paths.append( (framework, f"{board}/{filename}" if framework == "mxq" else filename) ) - if files.get("local_artifact_only"): + + def is_local_only(framework: str) -> bool: + return bool(files.get("local_artifact_only")) or ( + framework == "onnx" and bool(files.get("onnx_local_artifact_only")) + ) + + if all(is_local_only(framework) for framework, _ in paths): return tuple( HubArtifactCheck( repo_id, revision, None, framework, path, "local_artifact_only" @@ -89,10 +95,14 @@ def check_hub_artifacts( HubArtifactCheck( repo_id, revision, - info.sha, + None if is_local_only(framework) else info.sha, framework, path, - "resolved" if path in available else "missing", + "local_artifact_only" + if is_local_only(framework) + else "resolved" + if path in available + else "missing", ) for framework, path in paths ) diff --git a/mblt_vision/models/bases/imagenet_classifier.yaml b/mblt_vision/models/bases/imagenet_classifier.yaml index bc4b08b..ab0129e 100644 --- a/mblt_vision/models/bases/imagenet_classifier.yaml +++ b/mblt_vision/models/bases/imagenet_classifier.yaml @@ -1,5 +1,7 @@ # Shared ordered ImageNet preprocessing; model-specific values follow recorded ops inputs. DEFAULT: + file_cfg: + onnx_local_artifact_only: true pre_cfg: Reader: style: pil diff --git a/mblt_vision/wrapper.py b/mblt_vision/wrapper.py index d451284..fa5adb3 100644 --- a/mblt_vision/wrapper.py +++ b/mblt_vision/wrapper.py @@ -739,6 +739,11 @@ def file_config_cleansing(self) -> None: ) if onnx_filename and framework == "onnx" and not self.file_cfg.get("onnx_path"): + if self.file_cfg.get("onnx_local_artifact_only"): + raise RuntimeError( + f"'{repo_id}' publishes MXQ artifacts only. ONNX requires a trusted local " + "model_path or onnx_path; automatic ONNX downloads are disabled." + ) self.file_cfg["onnx_path"] = self._download_hub_artifact( repo_id=repo_id, filename=onnx_filename, diff --git a/tests/test_onnx_local_artifact.py b/tests/test_onnx_local_artifact.py new file mode 100644 index 0000000..342dcaf --- /dev/null +++ b/tests/test_onnx_local_artifact.py @@ -0,0 +1,76 @@ +"""Positive and negative CPU tests for framework-specific local-only artifacts.""" + +from __future__ import annotations + +from unittest.mock import Mock + +import pytest + +from mblt_vision._hub_validation import check_hub_artifacts +from mblt_vision.wrapper import MBLT_Engine, resolve_model_config + + +def engine_config(framework): + engine = object.__new__(MBLT_Engine) + engine.framework = framework + engine.file_cfg = resolve_model_config("EfficientViT_B3")["file_cfg"] + engine._download_hub_artifact = Mock(return_value="/downloaded/artifact") + return engine + + +def test_marked_remote_onnx_fails_without_download() -> None: + engine = engine_config("onnx") + with pytest.raises(RuntimeError, match="trusted local"): + engine.file_config_cleansing() + engine._download_hub_artifact.assert_not_called() + + +def test_marked_local_onnx_still_works(tmp_path) -> None: + engine = engine_config("onnx") + source = tmp_path / "trusted.onnx" + source.write_bytes(b"local fixture; runtime not constructed") + engine.file_cfg["onnx_path"] = str(source) + engine.file_config_cleansing() + assert engine.file_cfg["onnx_path"] == str(source) + engine._download_hub_artifact.assert_not_called() + + +def test_marked_mxq_keeps_hub_download() -> None: + engine = engine_config("mxq") + engine.file_config_cleansing() + engine._download_hub_artifact.assert_called_once_with( + repo_id="mobilint/EfficientViT_B3", + filename="efficientvit_b3.mxq", + revision="main", + subfolders=["aries-rb"], + ) + + +def test_unmarked_remote_onnx_keeps_hub_download() -> None: + engine = engine_config("onnx") + engine.file_cfg.pop("onnx_local_artifact_only") + engine.file_config_cleansing() + engine._download_hub_artifact.assert_called_once_with( + repo_id="mobilint/EfficientViT_B3", + filename="efficientvit_b3.onnx", + revision="main", + ) + + +def test_verifier_skips_only_marked_framework() -> None: + api = Mock() + api.model_info.return_value.sha = "observed" + api.list_repo_files.return_value = ["aries-rb/efficientvit_b3.mxq"] + checks = check_hub_artifacts(resolve_model_config("EfficientViT_B3"), api=api) + assert [item.status for item in checks] == ["resolved", "local_artifact_only"] + assert checks[1].commit is None + assert api.model_info.call_count == 1 + + +def test_onnx_only_verification_stays_offline() -> None: + api = Mock() + checks = check_hub_artifacts( + resolve_model_config("EfficientViT_B3"), frameworks=("onnx",), api=api + ) + assert checks[0].status == "local_artifact_only" + assert not api.mock_calls diff --git a/tests/test_published_classifiers.py b/tests/test_published_classifiers.py index 27d23b3..c1439c3 100644 --- a/tests/test_published_classifiers.py +++ b/tests/test_published_classifiers.py @@ -70,6 +70,7 @@ def test_configs_match_recorded_ops_inputs_and_discover(model: str) -> None: assert config["post_cfg"] == {"task": "image_classification", "dataset": "imagenet"} assert config["file_cfg"]["repo_id"] == f"mobilint/{model}" assert config["file_cfg"]["revision"] == "main" + assert config["file_cfg"]["onnx_local_artifact_only"] is True assert ( model in mblt_vision.list_models("image_classification")["image_classification"] ) From 31ddef1a103793455476efbb0b082e00169b16be Mon Sep 17 00:00:00 2001 From: jinman Date: Wed, 4 Nov 2026 16:49:53 +0900 Subject: [PATCH 5/6] Honor ONNX-only policy across backend and compile paths Exclude configuration-only marker from MXQ runtime kwargs, guard compilation downloads and cover local/unmarked positive paths. Co-Authored-By: Claude Haiku 5.5 --- mblt_vision/compile/vision.py | 6 ++++++ mblt_vision/wrapper.py | 1 + tests/test_onnx_local_artifact.py | 35 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/mblt_vision/compile/vision.py b/mblt_vision/compile/vision.py index 50560b2..fb2fb98 100644 --- a/mblt_vision/compile/vision.py +++ b/mblt_vision/compile/vision.py @@ -891,6 +891,12 @@ def _resolve_compile_onnx_path( "have a repository-pinned immutable revision and SHA-256 digest. Supply a " "trusted local model_path or onnx_path instead." ) + if file_cfg.get("onnx_local_artifact_only"): + repository = repo_id if isinstance(repo_id, str) and repo_id else "this model" + raise RuntimeError( + f"'{repository}' publishes MXQ artifacts only. ONNX compilation requires a trusted " + "local model_path or onnx_path; automatic ONNX downloads are disabled." + ) if isinstance(repo_id, str) and isinstance(revision, str) and filename: downloaded_path = Path( hf_hub_download( diff --git a/mblt_vision/wrapper.py b/mblt_vision/wrapper.py index fa5adb3..39f8dd0 100644 --- a/mblt_vision/wrapper.py +++ b/mblt_vision/wrapper.py @@ -628,6 +628,7 @@ def _mxq_backend_kwargs(self) -> dict[str, Any]: "revision", "onnx_filename", "onnx_path", + "onnx_local_artifact_only", } return { key: value diff --git a/tests/test_onnx_local_artifact.py b/tests/test_onnx_local_artifact.py index 342dcaf..a26a5d1 100644 --- a/tests/test_onnx_local_artifact.py +++ b/tests/test_onnx_local_artifact.py @@ -7,6 +7,7 @@ import pytest from mblt_vision._hub_validation import check_hub_artifacts +from mblt_vision.compile import vision as compile_module from mblt_vision.wrapper import MBLT_Engine, resolve_model_config @@ -44,6 +45,9 @@ def test_marked_mxq_keeps_hub_download() -> None: revision="main", subfolders=["aries-rb"], ) + kwargs = engine._mxq_backend_kwargs() + assert "onnx_local_artifact_only" not in kwargs + assert kwargs["mxq_path"] == "/downloaded/artifact" def test_unmarked_remote_onnx_keeps_hub_download() -> None: @@ -74,3 +78,34 @@ def test_onnx_only_verification_stays_offline() -> None: ) assert checks[0].status == "local_artifact_only" assert not api.mock_calls + + +def test_compile_resolver_requires_local_onnx(monkeypatch, tmp_path) -> None: + download = Mock() + monkeypatch.setattr(compile_module, "hf_hub_download", download) + config = resolve_model_config("EfficientViT_B3")["file_cfg"] + with pytest.raises(RuntimeError, match="trusted local"): + compile_module._resolve_compile_onnx_path(config, None) + download.assert_not_called() + local = tmp_path / "trusted.onnx" + local.write_bytes(b"fixture") + assert compile_module._resolve_compile_onnx_path(config, local) == local.resolve() + assert ( + compile_module._resolve_compile_onnx_path( + {**config, "onnx_path": str(local)}, None + ) + == local.resolve() + ) + download.assert_not_called() + + +def test_compile_unmarked_config_retains_download(monkeypatch, tmp_path) -> None: + local = tmp_path / "downloaded.onnx" + local.write_bytes(b"fixture") + download = Mock(return_value=str(local)) + monkeypatch.setattr(compile_module, "hf_hub_download", download) + monkeypatch.setattr(compile_module, "get_mobilint_cache_dir", lambda: str(tmp_path)) + config = resolve_model_config("EfficientViT_B3")["file_cfg"] + config.pop("onnx_local_artifact_only") + assert compile_module._resolve_compile_onnx_path(config, None) == local.resolve() + download.assert_called_once() From aaf0c14c2453e28c01be221744b56332f42dbe7d Mon Sep 17 00:00:00 2001 From: jinman Date: Wed, 4 Nov 2026 16:57:23 +0900 Subject: [PATCH 6/6] Add pending EfficientViT B0 B1 B2 family sibling configs Use official timm defaults and no-network pending status for new sibling configurations only. Preserve all pre-existing YAML bytes and allow trusted local artifacts without inventing HF publication metadata. Co-Authored-By: Claude Haiku 5.5 --- .claude/skills/mblt-vision/SKILL.md | 3 + AGENTS.md | 3 + mblt_vision/README.md | 11 +++ mblt_vision/_hub_validation.py | 8 +++ mblt_vision/compile/vision.py | 11 ++- mblt_vision/image_classification/__init__.py | 6 ++ mblt_vision/models/EfficientViT_B0.yaml | 13 ++++ mblt_vision/models/EfficientViT_B1.yaml | 13 ++++ mblt_vision/models/EfficientViT_B2.yaml | 13 ++++ mblt_vision/wrapper.py | 6 ++ tests/test_pending_family_siblings.py | 70 ++++++++++++++++++++ 11 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 mblt_vision/models/EfficientViT_B0.yaml create mode 100644 mblt_vision/models/EfficientViT_B1.yaml create mode 100644 mblt_vision/models/EfficientViT_B2.yaml create mode 100644 tests/test_pending_family_siblings.py diff --git a/.claude/skills/mblt-vision/SKILL.md b/.claude/skills/mblt-vision/SKILL.md index 5de5a20..f8d3055 100644 --- a/.claude/skills/mblt-vision/SKILL.md +++ b/.claude/skills/mblt-vision/SKILL.md @@ -33,6 +33,9 @@ description: >- including the TF-normalized `ViT_Large_Patch14_CLIP_224` exception. Their MXQ-only repositories use `onnx_local_artifact_only: true`: require trusted local ONNX and retain automatic MXQ loading; metadata validation skips only the marked framework. +- New pending family siblings use `file_cfg.artifact_status: pending_hf_family_sibling` + and require trusted local artifacts for runtime/compile. Report them as pending without + network; retain byte-identical existing sibling YAMLs. - For Hub linkage changes, run `tests/test_hub_validation.py` as described in `tests/TEST.md`. The metadata checker follows runtime board paths without downloading weights; live commits are diagnostics, not substitutes for missing historical approval provenance. diff --git a/AGENTS.md b/AGENTS.md index 411570e..93e02fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,9 @@ The current ownership boundary is deliberate: `ViT_Large_Patch14_CLIP_224` rather than inferring normalization from its name. These 24 repositories are MXQ-only: their base uses `onnx_local_artifact_only: true`. Keep automatic MXQ loading enabled, require trusted local ONNX, and test both guard paths. + Newly added EfficientViT B0/B1/B2 sibling configs carry + `artifact_status: pending_hf_family_sibling`: verify as pending without network and + require trusted local runtime/compile inputs. Existing sibling configs stay unchanged. - To share processing defaults without changing legacy models, opt new family YAMLs into `extends: bases/family.yaml#DEFAULT`. See the configuration-inheritance section of `mblt_vision/README.md` for ordering and merge rules. Keep legacy `update` shallow and diff --git a/mblt_vision/README.md b/mblt_vision/README.md index 3e3c8bf..da1334e 100644 --- a/mblt_vision/README.md +++ b/mblt_vision/README.md @@ -124,6 +124,17 @@ but ONNX requires a trusted local `model_path` or `onnx_path`. Automatic ONNX loading fails with a specific message, and metadata verification skips only that framework. Existing configurations without this marker retain their behavior. +`EfficientViT_B0`, `EfficientViT_B1` and `EfficientViT_B2` are intentional family +sibling configurations using official timm 1.0.14 `r224_in1k` defaults (224 crop, +bicubic resize to `floor(224 / 0.95) = 235`, ImageNet normalization). They are +**pending HF (family sibling)**, not published-model claims. Their +`file_cfg.artifact_status: pending_hf_family_sibling` makes metadata verification +skip both frameworks without network access and allows runtime/compile use only +with trusted local artifacts. Existing sibling configurations are not rewritten. +EfficientViT M0–M5 is a distinct architecture and is not added here. Alternative +ViT CLIP/GAP resolutions require exact upstream squash-mode preprocessing support; +this change does not approximate them with center cropping. + | Model | Input Size
(H,W,C) | AccTop1
(NPU) | AccTop1
(GPU) | FLOPs (B) | params (M) | Source | Note | | --- | --- | --- | --- | --- | --- | --- | --- | | AlexNet | (224,224,3) | 56.084 | 56.556 | 1.43 | 61.10 | [Link](https://docs.pytorch.org/vision/stable/models/generated/torchvision.models.alexnet.html) | | diff --git a/mblt_vision/_hub_validation.py b/mblt_vision/_hub_validation.py index 901373b..04844d6 100644 --- a/mblt_vision/_hub_validation.py +++ b/mblt_vision/_hub_validation.py @@ -70,6 +70,14 @@ def check_hub_artifacts( (framework, f"{board}/{filename}" if framework == "mxq" else filename) ) + if files.get("artifact_status") == "pending_hf_family_sibling": + return tuple( + HubArtifactCheck( + repo_id, revision, None, framework, path, "pending_hf_family_sibling" + ) + for framework, path in paths + ) + def is_local_only(framework: str) -> bool: return bool(files.get("local_artifact_only")) or ( framework == "onnx" and bool(files.get("onnx_local_artifact_only")) diff --git a/mblt_vision/compile/vision.py b/mblt_vision/compile/vision.py index fb2fb98..d57cbf8 100644 --- a/mblt_vision/compile/vision.py +++ b/mblt_vision/compile/vision.py @@ -786,8 +786,10 @@ def resolve_quantization_values( _validate_ratio("topk_ratio", topk_ratio) if topk_ratio is not None else None ) hosted_config: dict[str, Any] | None = None - if (resolved_percentile is None or resolved_topk is None) and not file_cfg.get( - "local_artifact_only" + if ( + (resolved_percentile is None or resolved_topk is None) + and not file_cfg.get("local_artifact_only") + and file_cfg.get("artifact_status") != "pending_hf_family_sibling" ): repo_id = file_cfg.get("repo_id") revision = file_cfg.get("revision", "main") @@ -884,6 +886,11 @@ def _resolve_compile_onnx_path( repo_id = file_cfg.get("repo_id") revision = file_cfg.get("revision") filename = _configured_onnx_filename(file_cfg) + if file_cfg.get("artifact_status") == "pending_hf_family_sibling": + raise RuntimeError( + "This configuration is pending HF (family sibling). Supply a trusted local " + "model_path or onnx_path for compilation; automatic downloads are disabled." + ) if file_cfg.get("local_artifact_only"): repository = repo_id if isinstance(repo_id, str) and repo_id else "this model" raise RuntimeError( diff --git a/mblt_vision/image_classification/__init__.py b/mblt_vision/image_classification/__init__.py index 085defb..4bcab7c 100644 --- a/mblt_vision/image_classification/__init__.py +++ b/mblt_vision/image_classification/__init__.py @@ -58,6 +58,9 @@ "EfficientNet_V2_L", "EfficientNet_V2_M", "EfficientNet_V2_S", + "EfficientViT_B0", + "EfficientViT_B1", + "EfficientViT_B2", "EfficientViT_B3", "EfficientViT_L1", "EfficientViT_L2", @@ -182,6 +185,9 @@ "YOLOv8xCls", ] +EfficientViT_B0 = create_model_class("EfficientViT_B0", __name__) +EfficientViT_B1 = create_model_class("EfficientViT_B1", __name__) +EfficientViT_B2 = create_model_class("EfficientViT_B2", __name__) EfficientViT_B3 = create_model_class("EfficientViT_B3", __name__) EfficientViT_L1 = create_model_class("EfficientViT_L1", __name__) EfficientViT_L2 = create_model_class("EfficientViT_L2", __name__) diff --git a/mblt_vision/models/EfficientViT_B0.yaml b/mblt_vision/models/EfficientViT_B0.yaml new file mode 100644 index 0000000..406152d --- /dev/null +++ b/mblt_vision/models/EfficientViT_B0.yaml @@ -0,0 +1,13 @@ +# Official timm 1.0.14 efficientvit_b0.r224_in1k pretrained_cfg: +# 224 crop, bicubic, crop_pct 0.95 -> floor(224 / 0.95) = 235, ImageNet torch stats. +# Family sibling configuration; no published artifact or approval is claimed. +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/EfficientViT_B0 + filename: efficientvit_b0.mxq + revision: main + artifact_status: pending_hf_family_sibling + pre_cfg: + Resize: + size: 235 diff --git a/mblt_vision/models/EfficientViT_B1.yaml b/mblt_vision/models/EfficientViT_B1.yaml new file mode 100644 index 0000000..9bda945 --- /dev/null +++ b/mblt_vision/models/EfficientViT_B1.yaml @@ -0,0 +1,13 @@ +# Official timm 1.0.14 efficientvit_b1.r224_in1k pretrained_cfg: +# 224 crop, bicubic, crop_pct 0.95 -> floor(224 / 0.95) = 235, ImageNet torch stats. +# Family sibling configuration; no published artifact or approval is claimed. +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/EfficientViT_B1 + filename: efficientvit_b1.mxq + revision: main + artifact_status: pending_hf_family_sibling + pre_cfg: + Resize: + size: 235 diff --git a/mblt_vision/models/EfficientViT_B2.yaml b/mblt_vision/models/EfficientViT_B2.yaml new file mode 100644 index 0000000..879995c --- /dev/null +++ b/mblt_vision/models/EfficientViT_B2.yaml @@ -0,0 +1,13 @@ +# Official timm 1.0.14 efficientvit_b2.r224_in1k pretrained_cfg: +# 224 crop, bicubic, crop_pct 0.95 -> floor(224 / 0.95) = 235, ImageNet torch stats. +# Family sibling configuration; no published artifact or approval is claimed. +DEFAULT: + extends: bases/imagenet_classifier.yaml + file_cfg: + repo_id: mobilint/EfficientViT_B2 + filename: efficientvit_b2.mxq + revision: main + artifact_status: pending_hf_family_sibling + pre_cfg: + Resize: + size: 235 diff --git a/mblt_vision/wrapper.py b/mblt_vision/wrapper.py index 39f8dd0..9f99a43 100644 --- a/mblt_vision/wrapper.py +++ b/mblt_vision/wrapper.py @@ -629,6 +629,7 @@ def _mxq_backend_kwargs(self) -> dict[str, Any]: "onnx_filename", "onnx_path", "onnx_local_artifact_only", + "artifact_status", } return { key: value @@ -723,6 +724,11 @@ def file_config_cleansing(self) -> None: revision = self.file_cfg.pop("revision", None) if not repo_id or not revision: return + if self.file_cfg.get("artifact_status") == "pending_hf_family_sibling": + raise RuntimeError( + f"'{repo_id}' is pending HF (family sibling). Supply a trusted local " + "model_path, mxq_path, or onnx_path; automatic downloads are disabled." + ) if local_artifact_only: raise RuntimeError( f"Remote artifacts for '{repo_id}' are disabled because this model does not " diff --git a/tests/test_pending_family_siblings.py b/tests/test_pending_family_siblings.py new file mode 100644 index 0000000..b33dec5 --- /dev/null +++ b/tests/test_pending_family_siblings.py @@ -0,0 +1,70 @@ +"""Intentional family-sibling configs are discoverable but never auto-download.""" + +from __future__ import annotations + +from unittest.mock import Mock + +import pytest + +import mblt_vision +from mblt_vision._hub_validation import check_hub_artifacts +from mblt_vision.compile import vision as compile_module +from mblt_vision.wrapper import MBLT_Engine, resolve_model_config + + +@pytest.mark.parametrize( + "name", ["EfficientViT_B0", "EfficientViT_B1", "EfficientViT_B2"] +) +def test_official_sibling_pipeline_and_pending_metadata(name) -> None: + config = resolve_model_config(name) + reference = resolve_model_config("EfficientViT_B3") + assert config["pre_cfg"] == reference["pre_cfg"] + assert config["pre_cfg"]["Resize"] == {"size": 235, "interpolation": "bicubic"} + assert config["file_cfg"]["artifact_status"] == "pending_hf_family_sibling" + assert ( + name in mblt_vision.list_models("image_classification")["image_classification"] + ) + assert getattr(mblt_vision, name) is getattr(mblt_vision.image_classification, name) + api = Mock() + checks = check_hub_artifacts(config, api=api) + assert all(item.status == "pending_hf_family_sibling" for item in checks) + assert not api.mock_calls + + +@pytest.mark.parametrize("framework", ["mxq", "onnx"]) +def test_pending_engine_never_downloads(framework) -> None: + engine = object.__new__(MBLT_Engine) + engine.framework = framework + engine.file_cfg = resolve_model_config("EfficientViT_B0")["file_cfg"] + engine._download_hub_artifact = Mock() + with pytest.raises(RuntimeError, match="pending HF"): + engine.file_config_cleansing() + engine._download_hub_artifact.assert_not_called() + + +@pytest.mark.parametrize("framework", ["mxq", "onnx"]) +def test_pending_engine_accepts_trusted_local_artifact(framework, tmp_path) -> None: + engine = object.__new__(MBLT_Engine) + engine.framework = framework + engine.file_cfg = resolve_model_config("EfficientViT_B0")["file_cfg"] + source = tmp_path / f"trusted.{framework}" + source.write_bytes(b"fixture") + engine.file_cfg[f"{framework}_path"] = str(source) + engine._download_hub_artifact = Mock() + engine.file_config_cleansing() + assert "artifact_status" not in engine._mxq_backend_kwargs() + engine._download_hub_artifact.assert_not_called() + + +def test_pending_compile_never_downloads_and_accepts_local( + monkeypatch, tmp_path +) -> None: + config = resolve_model_config("EfficientViT_B0")["file_cfg"] + download = Mock() + monkeypatch.setattr(compile_module, "hf_hub_download", download) + with pytest.raises(RuntimeError, match="pending HF"): + compile_module._resolve_compile_onnx_path(config, None) + local = tmp_path / "trusted.onnx" + local.write_bytes(b"fixture") + assert compile_module._resolve_compile_onnx_path(config, local) == local.resolve() + download.assert_not_called()