Skip to content

Commit f831830

Browse files
authored
Fix SAM wrapper resolution after task normalization (#1403)
## Summary - resolve explicit ModelKit custom wrappers using the original user task before the normalized Optimum task - keep the normalized-task lookup as a fallback - add regression coverage for `SAMMaskGeneration` and `SAM2MaskGeneration` ## Validation - `D:\Dev\MKs\ModelKit\.venv\Scripts\python.exe -m pytest tests\unit\loader\test_resolve_task.py -q` - 22 passed - Intel OpenVINO NPU targeted rerun for 10 SAM/SAM2 model keys across `w8a8` and `w8a16` - original model-class lookup error: 0/20 - perf: 18/20 passed - remaining two failures are `wanglab/medsam-vit-base` TensorFlow-only weight export failures and are unrelated to wrapper resolution Fixes #1402
1 parent c7329de commit f831830

2 files changed

Lines changed: 93 additions & 6 deletions

File tree

src/winml/modelkit/loader/resolution.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,9 +536,16 @@ def resolve_task(
536536
# CLIP --model-class override).
537537
resolved = None
538538
if model_type_norm:
539-
custom = _get_custom_model_class(model_type_norm, opt_task)
540-
if custom is not None and custom.__name__ == model_class:
541-
resolved = custom
539+
candidate_tasks = [opt_task]
540+
if task is not None:
541+
candidate_tasks.insert(0, task)
542+
for candidate_task in candidate_tasks:
543+
custom = _get_custom_model_class(model_type_norm, candidate_task)
544+
if custom is not None and custom.__name__ == model_class:
545+
resolved = custom
546+
if task is not None and candidate_task == task:
547+
surfaced = _resolve_task_modality(config, task)
548+
break
542549
if resolved is None:
543550
try:
544551
resolved = TasksManager.get_model_class_for_task(

tests/unit/loader/test_resolve_task.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import pytest
1212
from transformers import AutoConfig
1313

14+
from winml.modelkit.export.io import _get_onnx_config
15+
from winml.modelkit.loader import resolve_loader_config
1416
from winml.modelkit.loader.resolution import TaskSource, resolve_task
1517
from winml.modelkit.loader.task import to_optimum_task
1618

@@ -106,6 +108,86 @@ def test_user_class_unknown_raises_friendly_error():
106108
resolve_task(cfg, model_class="NotARealClass")
107109

108110

111+
@pytest.mark.parametrize(
112+
("model_type", "model_class", "io_config_class", "expected_inputs"),
113+
[
114+
(
115+
"sam",
116+
"SAMMaskGeneration",
117+
"SamMaskGenerationIOConfig",
118+
{"input_points", "input_labels", "image_embeddings", "mask_input"},
119+
),
120+
(
121+
"sam2",
122+
"SAM2MaskGeneration",
123+
"Sam2MaskGenerationIOConfig",
124+
{"input_points", "image_embeddings", "high_res_features0", "high_res_features1"},
125+
),
126+
(
127+
"sam2_video",
128+
"SAM2MaskGeneration",
129+
"Sam2MaskGenerationIOConfig",
130+
{"input_points", "image_embeddings", "high_res_features0", "high_res_features1"},
131+
),
132+
],
133+
)
134+
def test_user_class_custom_wrapper_preserves_task_and_round_trips_loader_config(
135+
model_type, model_class, io_config_class, expected_inputs
136+
):
137+
loader_config, hf_config, _, resolution = resolve_loader_config(
138+
model_type=model_type,
139+
task="mask-generation",
140+
model_class=model_class,
141+
)
142+
143+
assert loader_config.task == "mask-generation"
144+
assert resolution.optimum_task == "mask-generation"
145+
146+
round_trip = resolve_task(
147+
hf_config,
148+
task=loader_config.task,
149+
model_class=loader_config.model_class,
150+
)
151+
assert round_trip.task == "mask-generation"
152+
assert round_trip.optimum_task == "mask-generation"
153+
assert round_trip.model_class.__name__ == model_class
154+
155+
onnx_config = _get_onnx_config(loader_config.model_type, loader_config.task, hf_config)
156+
assert type(onnx_config).__name__ == io_config_class
157+
assert expected_inputs <= onnx_config.inputs.keys()
158+
159+
160+
@pytest.mark.parametrize("model_type", ["sam2", "sam2_video"])
161+
def test_user_class_custom_wrapper_tries_normalized_task_after_name_mismatch(model_type):
162+
r = resolve_task(
163+
_cfg(model_type, ["Sam2Model"]),
164+
task="mask-generation",
165+
model_class="Sam2VisionEncoder",
166+
)
167+
168+
assert r.task == "image-feature-extraction"
169+
assert r.optimum_task == "feature-extraction"
170+
assert r.model_class.__name__ == "Sam2VisionEncoder"
171+
172+
173+
@pytest.mark.parametrize("model_type", ["sam2", "sam2_video"])
174+
def test_user_class_custom_wrapper_preserves_modality_for_canonical_task(model_type):
175+
config = _cfg(model_type, ["Sam2Model"])
176+
expected = resolve_task(
177+
config,
178+
task="mask-generation",
179+
model_class="Sam2VisionEncoder",
180+
)
181+
182+
resolution = resolve_task(
183+
config,
184+
task=expected.optimum_task,
185+
model_class=expected.model_class.__name__,
186+
)
187+
188+
assert resolution == expected
189+
190+
109191
def test_user_task_unsupported_raises_friendly_error():
110192
cfg = _cfg("bert", ["BertModel"])
111193
with pytest.raises(ValueError, match="not supported by TasksManager"):
@@ -173,9 +255,7 @@ def test_user_class_explicit_feature_extraction_is_modality_aware():
173255
surface image-feature-extraction (-> ImageDataset), not the modality-blind
174256
feature-extraction (-> TextDataset). optimum_task still collapses for the Optimum
175257
class lookup."""
176-
r = resolve_task(
177-
_cfg("vit", ["ViTModel"]), model_class="ViTModel", task="feature-extraction"
178-
)
258+
r = resolve_task(_cfg("vit", ["ViTModel"]), model_class="ViTModel", task="feature-extraction")
179259
assert r.source == TaskSource.USER_CLASS
180260
assert r.task == "image-feature-extraction"
181261
assert r.optimum_task == "feature-extraction"

0 commit comments

Comments
 (0)