diff --git a/modelscope/utils/automodel_utils.py b/modelscope/utils/automodel_utils.py index 8acec9f5..73175dc6 100644 --- a/modelscope/utils/automodel_utils.py +++ b/modelscope/utils/automodel_utils.py @@ -141,7 +141,7 @@ def check_model_from_owner_group(model_dir: str, return False if owner_group is None: owner_group = ['iic', 'damo'] - model_dir = model_dir.rstrip('/').rstrip('\\') + model_dir = os.path.normpath(model_dir.rstrip('/').rstrip('\\')) parent_dir = os.path.dirname(model_dir) group = os.path.basename(parent_dir) if group in owner_group: diff --git a/modelscope/utils/hf_util/patcher.py b/modelscope/utils/hf_util/patcher.py index e4266e08..ee26d3d5 100644 --- a/modelscope/utils/hf_util/patcher.py +++ b/modelscope/utils/hf_util/patcher.py @@ -155,6 +155,79 @@ def _decide_allow_file_pattern(module_name, cls=None): return extra_allow_file_pattern +def _get_class_from_dynamic_module(class_reference, *args, **kwargs): + """Wrapper that redirects dynamic-module downloads to ModelScope. + + When a config's ``auto_map`` references another repo, transformers calls + ``get_class_from_dynamic_module`` to fetch it. This wrapper ensures that + fetch goes through ModelScope instead of HuggingFace. + """ + from transformers.dynamic_module_utils import origin_get_class_from_dynamic_module + if 'pretrained_model_name_or_path' in inspect.signature( + origin_get_class_from_dynamic_module).parameters: + pretrained_model_name_or_path = args[0] + if not os.path.exists(pretrained_model_name_or_path): + from modelscope import snapshot_download + args[0] = snapshot_download(pretrained_model_name_or_path) + if '--' in class_reference: + repo_id, class_reference = class_reference.split('--') + if not os.path.exists(repo_id): + download_kwargs = {} + extra_allow_file_pattern = _decide_allow_file_pattern( + class_reference) + if extra_allow_file_pattern is not None: + download_kwargs[ + 'allow_file_pattern'] = extra_allow_file_pattern + if 'Config' in class_reference or 'Processor' in class_reference or 'Tokenizer' in class_reference: + download_kwargs['ignore_file_pattern'] = ignore_file_pattern + from modelscope import snapshot_download + repo_id = snapshot_download(repo_id, **download_kwargs) + class_reference = repo_id + '--' + class_reference + return origin_get_class_from_dynamic_module(class_reference, *args, + **kwargs) + + +def _patch_dynamic_module(): + """Globally patch ``get_class_from_dynamic_module`` to redirect to ModelScope.""" + from transformers import dynamic_module_utils + if not hasattr(dynamic_module_utils, + 'origin_get_class_from_dynamic_module'): + dynamic_module_utils.origin_get_class_from_dynamic_module = dynamic_module_utils.get_class_from_dynamic_module + dynamic_module_utils.get_class_from_dynamic_module = _get_class_from_dynamic_module + from transformers.models.auto import configuration_auto + configuration_auto.get_class_from_dynamic_module = _get_class_from_dynamic_module + + +def _unpatch_dynamic_module(): + from transformers import dynamic_module_utils + if hasattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module'): + dynamic_module_utils.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module + from transformers.models.auto import configuration_auto + configuration_auto.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module + delattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module') + + +@contextlib.contextmanager +def _dynamic_module_patch_scope(): + """Temporarily patch ``get_class_from_dynamic_module`` for the duration of + the ``with`` block, unless an outer patch (e.g. ``patch_hub()``) is already + in effect. + + This ensures that ``auto_map`` references inside a config are resolved via + ModelScope when loading through a modelscope-wrapped class, without + polluting the global transformers state for unrelated callers. + """ + from transformers import dynamic_module_utils + if hasattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module'): + yield + return + _patch_dynamic_module() + try: + yield + finally: + _unpatch_dynamic_module() + + def _patch_pretrained_class(all_imported_modules, wrap=False): """Patch all class to download from modelscope @@ -272,8 +345,9 @@ def _patch_pretrained_class(all_imported_modules, wrap=False): cls=module_class, **kwargs) - module_obj = module_class.from_pretrained(model, model_dir, - *model_args, **kwargs) + with _dynamic_module_patch_scope(): + module_obj = module_class.from_pretrained( + model, model_dir, *model_args, **kwargs) return module_obj @@ -286,8 +360,9 @@ def _patch_pretrained_class(all_imported_modules, wrap=False): model_dir = get_model_dir(pretrained_model_name_or_path, **kwargs) - module_obj = module_class.from_pretrained( - model_dir, *model_args, **kwargs) + with _dynamic_module_patch_scope(): + module_obj = module_class.from_pretrained( + model_dir, *model_args, **kwargs) if module_class.__name__.startswith('AutoModel'): module_obj.model_dir = model_dir @@ -315,8 +390,9 @@ def _patch_pretrained_class(all_imported_modules, wrap=False): allow_file_pattern=allow_file_pattern, **kwargs) - module_obj = module_class.get_config_dict( - model_dir, *model_args, **kwargs) + with _dynamic_module_patch_scope(): + module_obj = module_class.get_config_dict( + model_dir, *model_args, **kwargs) return module_obj def save_pretrained( @@ -441,39 +517,13 @@ def _patch_pretrained_class(all_imported_modules, wrap=False): all_available_modules.append(var) - def get_class_from_dynamic_module(class_reference, *args, **kwargs): - from transformers.dynamic_module_utils import origin_get_class_from_dynamic_module - if 'pretrained_model_name_or_path' in inspect.signature( - origin_get_class_from_dynamic_module).parameters: - pretrained_model_name_or_path = args[0] - if not os.path.exists(pretrained_model_name_or_path): - from modelscope import snapshot_download - args[0] = snapshot_download(pretrained_model_name_or_path) - if '--' in class_reference: - repo_id, class_reference = class_reference.split('--') - if not os.path.exists(repo_id): - download_kwargs = {} - extra_allow_file_pattern = _decide_allow_file_pattern( - class_reference) - if extra_allow_file_pattern is not None: - download_kwargs[ - 'allow_file_pattern'] = extra_allow_file_pattern - if 'Config' in class_reference or 'Processor' in class_reference or 'Tokenizer' in class_reference: - download_kwargs[ - 'ignore_file_pattern'] = ignore_file_pattern - from modelscope import snapshot_download - repo_id = snapshot_download(repo_id, **download_kwargs) - class_reference = repo_id + '--' + class_reference - return origin_get_class_from_dynamic_module(class_reference, *args, - **kwargs) - - from transformers import dynamic_module_utils - if not hasattr(dynamic_module_utils, - 'origin_get_class_from_dynamic_module'): - dynamic_module_utils.origin_get_class_from_dynamic_module = dynamic_module_utils.get_class_from_dynamic_module - dynamic_module_utils.get_class_from_dynamic_module = get_class_from_dynamic_module - from transformers.models.auto import configuration_auto - configuration_auto.get_class_from_dynamic_module = get_class_from_dynamic_module + # Only apply the global get_class_from_dynamic_module monkey-patch in + # direct-patch mode (wrap=False). In wrap mode the ClassWrapper scopes + # the patch to each from_pretrained / get_config_dict call via + # _dynamic_module_patch_scope(), so the global state stays clean for + # unrelated ``from transformers import AutoConfig`` callers (issue #1751). + if not wrap: + _patch_dynamic_module() return all_available_modules @@ -509,12 +559,7 @@ def _unpatch_pretrained_class(all_imported_modules): if has_get_config_dict and hasattr(var, '_get_config_dict_origin'): _restore(var, 'get_config_dict', '_get_config_dict_origin') - from transformers import dynamic_module_utils - if hasattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module'): - dynamic_module_utils.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module - from transformers.models.auto import configuration_auto - configuration_auto.get_class_from_dynamic_module = dynamic_module_utils.origin_get_class_from_dynamic_module - delattr(dynamic_module_utils, 'origin_get_class_from_dynamic_module') + _unpatch_dynamic_module() def _patch_kernels(): diff --git a/tests/utils/test_hf_util.py b/tests/utils/test_hf_util.py index 57695068..1dd52b39 100644 --- a/tests/utils/test_hf_util.py +++ b/tests/utils/test_hf_util.py @@ -263,6 +263,70 @@ class HFUtilTest(unittest.TestCase): f"{[f for f in files if f.endswith('.safetensors') or f.endswith('.bin')]}" ) + def test_import_not_pollute_dynamic_module(self): + """Importing from modelscope must not globally patch + transformers' get_class_from_dynamic_module (issue #1751). + + The patch should be scoped to each from_pretrained / get_config_dict + call via _dynamic_module_patch_scope(), leaving the global state clean + for unrelated ``from transformers import AutoConfig`` callers. + """ + from modelscope import AutoConfig + from modelscope.utils.file_utils import get_modelscope_cache_dir + from transformers import dynamic_module_utils + + # 1. Importing AutoConfig from modelscope (wrap=True) must NOT + # globally patch get_class_from_dynamic_module. + self.assertFalse( + hasattr(dynamic_module_utils, + 'origin_get_class_from_dynamic_module'), + 'Importing AutoConfig from modelscope must not globally patch ' + 'get_class_from_dynamic_module (issue #1751)') + + # 2. The modelscope AutoConfig should still work with + # trust_remote_code, downloading through ModelScope (not HF). + model = 'nomic-ai/nomic-embed-text-v1.5' + config = AutoConfig.from_pretrained(model, trust_remote_code=True) + model_dir = config.name_or_path + + # Verify files are in the ModelScope cache, not the HF cache. + ms_cache = get_modelscope_cache_dir() + self.assertTrue( + os.path.realpath(model_dir).startswith(os.path.realpath(ms_cache)), + f'Model files should be in ModelScope cache ({ms_cache}), ' + f'but found at {model_dir}') + + # 3. After the call the global patch should still NOT be in effect + # (the _dynamic_module_patch_scope was temporary). + self.assertFalse( + hasattr(dynamic_module_utils, + 'origin_get_class_from_dynamic_module'), + 'Global get_class_from_dynamic_module should not be patched ' + 'after a modelscope AutoConfig call (issue #1751)') + + # 4. Pure transformers AutoConfig (without modelscope patching) must + # NOT be redirected to ModelScope. With the old code the global + # patch would intercept this call and download from ModelScope; + # with the fix the call goes to HuggingFace directly. + from transformers import AutoConfig as HFAutoConfig + try: + hf_config = HFAutoConfig.from_pretrained( + model, trust_remote_code=True) + hf_model_dir = hf_config.name_or_path + # If the download succeeded, files must be outside the + # ModelScope cache (i.e. in the HuggingFace cache). + self.assertFalse( + os.path.realpath(hf_model_dir).startswith( + os.path.realpath(ms_cache)), + f'Transformers AutoConfig should download from HuggingFace, ' + f'not ModelScope (issue #1751). Found files at: ' + f'{hf_model_dir}') + except Exception: + # If HuggingFace is unreachable the call fails — which is also + # correct: it means the request did NOT go through ModelScope + # (which would have succeeded). + pass + @unittest.skipUnless(test_level() >= 1, 'skip test in current test level') def test_push_to_hub(self): with patch_context():