From 6548c7e65a1abde4c12ec7e268b31b63d791163c Mon Sep 17 00:00:00 2001 From: James Pine Date: Fri, 17 Apr 2026 16:47:44 -0700 Subject: [PATCH] fix(build): force transformers torch<2.6 mask path and bundle spacy_pkuseg - patch transformers.masking_utils to set _is_torch_greater_or_equal_than_2_6 = False, forcing sdpa_mask_older_torch and avoiding the vmap .item() crash that breaks Qwen CustomVoice generation (our torch._dynamo stub can't reproduce TransformGetItemToIndex's graph transform). - add PyInstaller hook to bundle transformers.masking_utils .py source so the runtime finder can source-patch it. - --collect-all spacy_pkuseg so Chatterbox Multilingual can load its Chinese segmenter (dicts/default.pkl + native .so extensions). - add per-finder install diagnostics + _HOOK_VERSION marker to make future bundle-only regressions easier to triage. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/build_binary.py | 5 + .../hook-transformers.masking_utils.py | 11 ++ backend/pyi_rth_torch_compiler_disable.py | 128 +++++++++++++++++- backend/voicebox-server.spec | 2 + 4 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 backend/pyi_hooks/hook-transformers.masking_utils.py diff --git a/backend/build_binary.py b/backend/build_binary.py index 2bf46b9d..2df2ceb2 100644 --- a/backend/build_binary.py +++ b/backend/build_binary.py @@ -135,6 +135,11 @@ def build_server(cuda=False): "backend.backends.chatterbox_backend", "--hidden-import", "backend.backends.chatterbox_turbo_backend", + # chatterbox multilingual uses spacy_pkuseg for Chinese word + # segmentation, which ships pickled dict files (dicts/default.pkl) + # and native .so extensions that --hidden-import alone won't bundle. + "--collect-all", + "spacy_pkuseg", "--hidden-import", "backend.backends.luxtts_backend", "--hidden-import", diff --git a/backend/pyi_hooks/hook-transformers.masking_utils.py b/backend/pyi_hooks/hook-transformers.masking_utils.py new file mode 100644 index 00000000..e18e9836 --- /dev/null +++ b/backend/pyi_hooks/hook-transformers.masking_utils.py @@ -0,0 +1,11 @@ +""" +Force transformers.masking_utils to be bundled with its .py source alongside +the .pyc bytecode so the runtime hook in +backend/pyi_rth_torch_compiler_disable.py can source-patch it. + +The patch forces the torch<2.6 code path, bypassing `with TransformGetItemToIndex()` +which our torch._dynamo no-op stub can't implement for real — the real context +manager uses dynamo graph transforms to avoid `.item()` calls inside vmap. +""" + +module_collection_mode = "pyz+py" diff --git a/backend/pyi_rth_torch_compiler_disable.py b/backend/pyi_rth_torch_compiler_disable.py index c3c327d2..e8810079 100644 --- a/backend/pyi_rth_torch_compiler_disable.py +++ b/backend/pyi_rth_torch_compiler_disable.py @@ -71,7 +71,8 @@ def _diag(msg: str) -> None: pass -_diag(f"=== runtime hook load @ pid={os.getpid()} ===") +_HOOK_VERSION = "v6-masking-utils-finder" +_diag(f"=== runtime hook load @ pid={os.getpid()} version={_HOOK_VERSION} ===") class _NoopDecorator: @@ -235,6 +236,111 @@ def _patch_scipy_distn_source(source: str) -> str: return source +def _patch_masking_utils_source(source: str) -> str: + """Force torch<2.6 code path in transformers.masking_utils. + + The torch>=2.6 path uses `with TransformGetItemToIndex():` to allow + `.item()` calls inside vmap. That context manager is implemented via + torch._dynamo graph transforms, which our stub doesn't reproduce — it's + a no-op. The inner `_vmap_for_bhqkv` then crashes with: + + RuntimeError: vmap: It looks like you're calling .item() on a Tensor. + + Forcing the torch<2.6 flag off selects sdpa_mask_older_torch which uses + a different vmap pattern that does not hit .item() and does not need + TransformGetItemToIndex. + """ + target = 'is_torch_greater_or_equal("2.6", accept_dev=True)' + # Find the specific line that assigns _is_torch_greater_or_equal_than_2_6 + if "_is_torch_greater_or_equal_than_2_6 = " + target in source: + return source.replace( + "_is_torch_greater_or_equal_than_2_6 = " + target, + "_is_torch_greater_or_equal_than_2_6 = False", + 1, + ) + return source + + +class _SourcePatchingFinder: + """Generic delegate-and-wrap meta-path finder that patches a module's + source before exec'ing. + + Subclasses declare `target` (module fullname) and `patch` (str->str). + Requires the target module's .py source to be bundled (use a PyInstaller + hook setting module_collection_mode = "pyz+py"). + """ + + target: str + patch_fn: callable = None + + def find_spec(self, fullname, path=None, target=None): + if fullname != self.target: + return None + for finder in sys.meta_path: + if finder is self: + continue + find = getattr(finder, "find_spec", None) + if find is None: + continue + try: + real_spec = find(fullname, path, target) + except Exception: + continue + if real_spec is None or real_spec.loader is None: + continue + real_spec.loader = _SourcePatchLoader(real_spec.loader, self.patch_fn) + return real_spec + return None + + +class _SourcePatchLoader: + """Delegate loader that reads source via get_source, applies a patch, and + compile/exec's the patched text into module.__dict__. + """ + + def __init__(self, inner, patch_fn): + self._inner = inner + self._patch_fn = patch_fn + + def __getattr__(self, name): + return getattr(self._inner, name) + + def create_module(self, spec): + return self._inner.create_module(spec) + + def exec_module(self, module): + source = None + try: + source = self._inner.get_source(module.__name__) + except Exception as e: + _diag(f"[source-patch] get_source({module.__name__}) failed: {e!r}") + + if not source: + _diag( + f"[source-patch] no source for {module.__name__}; " + "falling back to inner exec_module (patch NOT applied)" + ) + self._inner.exec_module(module) + return + + patched = self._patch_fn(source) + _diag( + f"[source-patch] {module.__name__}: " + f"patched={patched is not source}, len={len(patched)}" + ) + spec = module.__spec__ + if spec is not None and spec.submodule_search_locations is not None: + module.__path__ = spec.submodule_search_locations + filename = getattr(self._inner, "path", module.__name__) + exec(compile(patched, filename, "exec"), module.__dict__) + _diag(f"[source-patch] {module.__name__} OK") + + +class _MaskingUtilsFinder(_SourcePatchingFinder): + target = "transformers.masking_utils" + patch_fn = staticmethod(_patch_masking_utils_source) + + class _ScipyDistnPatchingFinder: """Delegate-and-wrap finder for scipy.stats._distn_infrastructure. @@ -389,11 +495,19 @@ def _install_dynamo_stub() -> None: # paths reach sklearn -> scipy.stats which trips a separate crash) # - scipy.stats._distn_infrastructure -> real load with `obj` pre-bound, # so librosa -> scipy.signal -> scipy.stats loads cleanly - sys.meta_path.insert(0, _DynamoMetaPathFinder()) - sys.meta_path.insert(0, _TransformersStubFinder()) - sys.meta_path.insert(0, _ScipyDistnPatchingFinder()) + for _FinderCls in ( + _DynamoMetaPathFinder, + _TransformersStubFinder, + _ScipyDistnPatchingFinder, + _MaskingUtilsFinder, + ): + try: + sys.meta_path.insert(0, _FinderCls()) + _diag(f"installed finder: {_FinderCls.__name__}") + except Exception as e: + _diag(f"FAILED to install {_FinderCls.__name__}: {e!r}") _diag( - "finders installed. sys.meta_path head: " + "final sys.meta_path head: " + ", ".join(type(f).__name__ for f in sys.meta_path[:6]) ) @@ -407,10 +521,10 @@ def _install_dynamo_stub() -> None: try: _install_dynamo_stub() -except Exception: +except Exception as _e: # Best effort. If this fails the original NameError will surface when # transformers imports — no worse than not patching at all. - pass + _diag(f"_install_dynamo_stub FAILED: {_e!r}") # NOTE: we deliberately do NOT import torch or torch.compiler here. # Runtime hooks run before the app starts and before pyi_rth_numpy_compat diff --git a/backend/voicebox-server.spec b/backend/voicebox-server.spec index 7dee2735..4f7f49b7 100644 --- a/backend/voicebox-server.spec +++ b/backend/voicebox-server.spec @@ -18,6 +18,8 @@ hiddenimports += collect_submodules('jaraco') hiddenimports += collect_submodules('tada') hiddenimports += collect_submodules('mlx') hiddenimports += collect_submodules('mlx_audio') +tmp_ret = collect_all('spacy_pkuseg') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('zipvoice') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('linacodec')