diff --git a/tests/test_missing_torchvision_vlm.py b/tests/test_missing_torchvision_vlm.py new file mode 100644 index 0000000000..5bc72a21da --- /dev/null +++ b/tests/test_missing_torchvision_vlm.py @@ -0,0 +1,30 @@ +"""Regression test for unsloth#4202: detect missing torchvision so the loader can +surface the real cause instead of a misleading collator error.""" + +import importlib.util +from unittest import mock + +from unsloth.models.vision import _missing_torchvision_error + + +def test_error_text_mentions_torchvision_is_detected(): + err = ImportError("Qwen3VLVideoProcessor requires the Torchvision library but ...") + assert _missing_torchvision_error(err) is True + + +def test_torchvision_missing_is_detected_without_error(): + with mock.patch.object(importlib.util, "find_spec", return_value = None): + assert _missing_torchvision_error(None) is True + + +def test_torchvision_present_unrelated_error_is_not_flagged(): + sentinel = object() + with mock.patch.object(importlib.util, "find_spec", return_value = sentinel): + assert _missing_torchvision_error(ValueError("unrelated")) is False + assert _missing_torchvision_error(None) is False + + +def test_matches_real_environment(): + # find_spec is the source of truth when no error is supplied. + expected = importlib.util.find_spec("torchvision") is None + assert _missing_torchvision_error(None) is expected diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 182030c68b..85ed5ee0b1 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -449,6 +449,23 @@ def unsloth_base_fast_generate(self, *args, **kwargs): return output +def _missing_torchvision_error(error = None): + """True if a VLM processor failed to load due to missing torchvision (#4202). + + Checks availability directly first, then only the specific torchvision-required + error text (not any incidental "torchvision" substring like a model path).""" + import importlib.util + + if importlib.util.find_spec("torchvision") is None: + return True + if error is not None: + error_str = str(error).lower() + return ( + "requires the torchvision" in error_str or "no module named 'torchvision'" in error_str + ) + return False + + def _construct_vlm_processor_fallback(tokenizer_name, model_type, token, trust_remote_code): """Construct a VLM processor manually when AutoProcessor.from_pretrained fails. @@ -1154,6 +1171,7 @@ class FastBaseModel: except Exception: pass + _processor_load_error = None if (whisper_language and whisper_task) or auto_model.__name__.endswith( "ForConditionalGeneration" ): @@ -1166,7 +1184,8 @@ class FastBaseModel: task = whisper_task, trust_remote_code = trust_remote_code, ) - except Exception: + except Exception as e: + _processor_load_error = e tokenizer = None else: try: @@ -1176,7 +1195,8 @@ class FastBaseModel: token = token, trust_remote_code = trust_remote_code, ) - except: + except Exception as e: + _processor_load_error = e tokenizer = get_auto_processor( tokenizer_name, padding_side = "left", @@ -1200,7 +1220,16 @@ class FastBaseModel: ) if _fallback is not None: tokenizer = _fallback - if tokenizer is None: + # Missing torchvision silently degrades the VLM processor to a text-only + # tokenizer; surface the real cause instead of the later collator error (#4202). + if tokenizer is None or not hasattr(tokenizer, "image_processor"): + if _missing_torchvision_error(_processor_load_error): + raise ImportError( + f"Unsloth: Could not load the vision processor for `{tokenizer_name}` " + "because torchvision is not installed. transformers requires torchvision " + "for this model's vision (image/video) processors. Please install it, " + "e.g. `pip install torchvision`." + ) import sys print( f"Unsloth: Warning - VLM processor fallback returned None for model_type={model_type_arch}",