fix: propagate PYTHONPATH to child subprocesses, revert tokenizer patching
This commit is contained in:
parent
9330588015
commit
e25705a211
7 changed files with 10 additions and 170 deletions
|
|
@ -96,7 +96,6 @@ class ExportBackend:
|
|||
self.current_tokenizer = None
|
||||
self.is_vision = False
|
||||
self.is_peft = False
|
||||
self._resolved_model_name = ""
|
||||
|
||||
def cleanup_memory(self):
|
||||
"""Offload and delete all models from memory"""
|
||||
|
|
@ -154,7 +153,6 @@ class ExportBackend:
|
|||
|
||||
# Check if it's a LoRA adapter
|
||||
adapter_config = checkpoint_path_obj / "adapter_config.json"
|
||||
base_model = None
|
||||
if adapter_config.exists():
|
||||
# It's a LoRA - get base model to check vision
|
||||
base_model = get_base_model_from_lora(checkpoint_path)
|
||||
|
|
@ -166,21 +164,6 @@ class ExportBackend:
|
|||
# Check the model itself
|
||||
self.is_vision = is_vision_model(checkpoint_path)
|
||||
|
||||
# Resolve model name for tokenizer patching (base model for LoRA, path otherwise)
|
||||
resolved_model_name = base_model or checkpoint_path
|
||||
self._resolved_model_name = resolved_model_name
|
||||
|
||||
# Patch broken tokenizer_config.json on disk before loading.
|
||||
# Qwen3.5/GLM checkpoints saved by TRL inherit "TokenizersBackend"
|
||||
# from the HF upload — fix it so from_pretrained loads correctly
|
||||
# and subsequent save_pretrained writes the right class.
|
||||
from utils.transformers_version import patch_tokenizer_config
|
||||
patch_tokenizer_config(checkpoint_path, model_name=resolved_model_name)
|
||||
# Also patch subdirectories (TRL saves tokenizer in checkpoint dirs)
|
||||
for subdir in checkpoint_path_obj.iterdir():
|
||||
if subdir.is_dir() and (subdir / "tokenizer_config.json").exists():
|
||||
patch_tokenizer_config(str(subdir), model_name=resolved_model_name)
|
||||
|
||||
# Load model based on type
|
||||
if self.is_vision:
|
||||
logger.info("Loading as vision model...")
|
||||
|
|
@ -200,27 +183,6 @@ class ExportBackend:
|
|||
load_in_4bit=load_in_4bit,
|
||||
)
|
||||
|
||||
# Patch broken tokenizer_class (e.g. Qwen3.5/GLM "TokenizersBackend")
|
||||
from utils.transformers_version import patch_tokenizer_in_memory
|
||||
patch_tokenizer_in_memory(tokenizer, model_name=resolved_model_name)
|
||||
|
||||
# Wrap tokenizer.save_pretrained so that every subsequent call
|
||||
# (including internal ones from save_pretrained_merged /
|
||||
# save_pretrained_gguf) auto-patches the on-disk output.
|
||||
# Without this, the GGUF converter subprocess fails because
|
||||
# save_pretrained re-writes "TokenizersBackend" to the output dir.
|
||||
_orig_tok_save = tokenizer.save_pretrained
|
||||
_fix_model_name = resolved_model_name
|
||||
|
||||
def _save_and_patch(*args, **kwargs):
|
||||
result = _orig_tok_save(*args, **kwargs)
|
||||
_dir = args[0] if args else kwargs.get("save_directory")
|
||||
if _dir:
|
||||
patch_tokenizer_config(str(_dir), model_name=_fix_model_name)
|
||||
return result
|
||||
|
||||
tokenizer.save_pretrained = _save_and_patch
|
||||
|
||||
# Check if PEFT model
|
||||
self.is_peft = isinstance(model, (PeftModel, PeftModelForCausalLM))
|
||||
|
||||
|
|
@ -298,10 +260,6 @@ class ExportBackend:
|
|||
save_method=save_method
|
||||
)
|
||||
|
||||
# Fix broken tokenizer_class on disk (belt-and-suspenders)
|
||||
from utils.transformers_version import patch_tokenizer_config
|
||||
patch_tokenizer_config(save_directory, model_name=self._resolved_model_name)
|
||||
|
||||
# Write export metadata so the Chat page can identify the base model
|
||||
self._write_export_metadata(save_directory)
|
||||
logger.info(f"Model saved successfully to {save_directory}")
|
||||
|
|
@ -358,10 +316,6 @@ class ExportBackend:
|
|||
self.current_model.save_pretrained(save_directory)
|
||||
self.current_tokenizer.save_pretrained(save_directory)
|
||||
|
||||
# Fix broken tokenizer_class on disk (belt-and-suspenders)
|
||||
from utils.transformers_version import patch_tokenizer_config
|
||||
patch_tokenizer_config(save_directory, model_name=self._resolved_model_name)
|
||||
|
||||
# Write export metadata so the Chat page can identify the base model
|
||||
self._write_export_metadata(save_directory)
|
||||
logger.info(f"Model saved successfully to {save_directory}")
|
||||
|
|
@ -473,12 +427,6 @@ class ExportBackend:
|
|||
quantization_method=quant_method
|
||||
)
|
||||
|
||||
# Fix broken tokenizer_class in intermediate HF output
|
||||
# (save_pretrained wrapper handles pre-converter, this is
|
||||
# belt-and-suspenders for the final on-disk state)
|
||||
from utils.transformers_version import patch_tokenizer_config
|
||||
patch_tokenizer_config(model_save_path, model_name=self._resolved_model_name)
|
||||
|
||||
# Relocate GGUF artifacts into the export directory.
|
||||
# convert_to_gguf writes .gguf files to cwd (repo root)
|
||||
# because --outfile is a relative path like "model.Q4_K_M.gguf".
|
||||
|
|
@ -563,11 +511,6 @@ class ExportBackend:
|
|||
|
||||
self.current_model.save_pretrained(save_directory)
|
||||
self.current_tokenizer.save_pretrained(save_directory)
|
||||
|
||||
# Fix broken tokenizer_class on disk (belt-and-suspenders)
|
||||
from utils.transformers_version import patch_tokenizer_config
|
||||
patch_tokenizer_config(save_directory, model_name=self._resolved_model_name)
|
||||
|
||||
logger.info(f"Adapter saved successfully to {save_directory}")
|
||||
|
||||
# Push to hub if requested
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ def _activate_transformers_version(model_name: str, project_root: str) -> None:
|
|||
sys.path.insert(0, venv_t5)
|
||||
logger.info("Activated transformers 5.x from %s", venv_t5)
|
||||
else:
|
||||
|
||||
# Fallback: pip install at runtime (slower, ~10-15s)
|
||||
logger.warning(".venv_t5 not found at %s — installing at runtime", venv_t5)
|
||||
import subprocess as sp
|
||||
|
|
@ -63,6 +64,9 @@ def _activate_transformers_version(model_name: str, project_root: str) -> None:
|
|||
f"pip returncode: transformers={r1.returncode}, huggingface_hub={r2.returncode}"
|
||||
)
|
||||
sys.path.insert(0, venv_t5)
|
||||
# Propagate to child subprocesses (e.g. GGUF converter)
|
||||
_pp = os.environ.get("PYTHONPATH", "")
|
||||
os.environ["PYTHONPATH"] = venv_t5 + (os.pathsep + _pp if _pp else "")
|
||||
else:
|
||||
logger.info("Using default transformers (4.57.x) for %s", model_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -151,10 +151,6 @@ class InferenceBackend:
|
|||
token=hf_token if hf_token and hf_token.strip() else None,
|
||||
)
|
||||
|
||||
# Patch broken tokenizer_class (Qwen3.5/GLM "TokenizersBackend")
|
||||
from utils.transformers_version import patch_tokenizer_in_memory
|
||||
patch_tokenizer_in_memory(tokenizer, model_name=model_name)
|
||||
|
||||
# Apply inference optimization
|
||||
FastLanguageModel.for_inference(model)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ def _activate_transformers_version(model_name: str, project_root: str) -> None:
|
|||
f"pip returncode: transformers={r1.returncode}, huggingface_hub={r2.returncode}"
|
||||
)
|
||||
sys.path.insert(0, venv_t5)
|
||||
# Propagate to child subprocesses (e.g. GGUF converter)
|
||||
_pp = os.environ.get("PYTHONPATH", "")
|
||||
os.environ["PYTHONPATH"] = venv_t5 + (os.pathsep + _pp if _pp else "")
|
||||
else:
|
||||
logger.info("Using default transformers (4.57.x) for %s", model_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -193,9 +193,6 @@ class UnslothTrainer:
|
|||
load_in_4bit=load_in_4bit,
|
||||
token=hf_token,
|
||||
)
|
||||
# Patch broken tokenizer_class (Qwen3.5/GLM "TokenizersBackend")
|
||||
from utils.transformers_version import patch_tokenizer_in_memory
|
||||
patch_tokenizer_in_memory(self.tokenizer, model_name=model_name)
|
||||
logger.info("Loaded text model")
|
||||
|
||||
if self.should_stop:
|
||||
|
|
@ -1074,8 +1071,6 @@ class UnslothTrainer:
|
|||
self.trainer.save_model()
|
||||
self.tokenizer.save_pretrained(output_dir)
|
||||
self._patch_adapter_config(output_dir)
|
||||
# Fix broken tokenizer_class on saved checkpoints
|
||||
self._patch_tokenizer_class_all(output_dir)
|
||||
print(f"\nTraining stopped. Model saved to {output_dir}\n")
|
||||
self._update_progress(
|
||||
is_training=False,
|
||||
|
|
@ -1093,8 +1088,6 @@ class UnslothTrainer:
|
|||
self.trainer.save_model()
|
||||
self.tokenizer.save_pretrained(output_dir)
|
||||
self._patch_adapter_config(output_dir)
|
||||
# Fix broken tokenizer_class on saved checkpoints
|
||||
self._patch_tokenizer_class_all(output_dir)
|
||||
print(f"\nTraining completed! Model saved to {output_dir}\n")
|
||||
self._update_progress(
|
||||
is_training=False,
|
||||
|
|
@ -1139,13 +1132,6 @@ class UnslothTrainer:
|
|||
except Exception as e:
|
||||
logger.warning(f"Failed to patch adapter_config.json: {e}")
|
||||
|
||||
def _patch_tokenizer_class_all(self, output_dir: str):
|
||||
"""Patch broken tokenizer_class in output dir and all checkpoint subdirs."""
|
||||
from utils.transformers_version import patch_tokenizer_config
|
||||
import glob
|
||||
for f in glob.glob(os.path.join(output_dir, "**", "tokenizer_config.json"), recursive=True):
|
||||
patch_tokenizer_config(os.path.dirname(f), model_name=self.model_name)
|
||||
|
||||
def stop_training(self, save: bool = True):
|
||||
"""Stop ongoing training"""
|
||||
print(f"\nStopping training (save={save})...")
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ def _activate_transformers_version(model_name: str, project_root: str) -> None:
|
|||
f"pip returncode: transformers={r1.returncode}, huggingface_hub={r2.returncode}"
|
||||
)
|
||||
sys.path.insert(0, venv_t5)
|
||||
# Propagate to child subprocesses (e.g. GGUF converter)
|
||||
_pp = os.environ.get("PYTHONPATH", "")
|
||||
os.environ["PYTHONPATH"] = venv_t5 + (os.pathsep + _pp if _pp else "")
|
||||
else:
|
||||
logger.info("Using default transformers (4.57.x) for %s", model_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -217,101 +217,6 @@ def _deactivate_5x() -> None:
|
|||
logger.info("Reverted to transformers %s", transformers.__version__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tokenizer patches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Some HF model uploads ship with tokenizer_class "TokenizersBackend"
|
||||
# instead of the real class. This causes llama.cpp's GGUF converter to fail.
|
||||
# Map: lowered model substring → correct tokenizer_class.
|
||||
_TOKENIZER_CLASS_OVERRIDES: dict[str, str] = {
|
||||
"qwen3.5": "Qwen2Tokenizer",
|
||||
"glm-4.7": "PreTrainedTokenizer",
|
||||
}
|
||||
|
||||
|
||||
def _get_tokenizer_class_fix(model_name: str) -> str | None:
|
||||
"""Return the correct tokenizer_class for a model, or None if no fix needed."""
|
||||
lowered = model_name.lower()
|
||||
for substr, fixed_class in _TOKENIZER_CLASS_OVERRIDES.items():
|
||||
if substr in lowered:
|
||||
return fixed_class
|
||||
return None
|
||||
|
||||
|
||||
def patch_tokenizer_config(model_dir: str, model_name: str = "") -> bool:
|
||||
"""Fix known broken tokenizer_class values in tokenizer_config.json.
|
||||
|
||||
Some HF uploads (Qwen3.5, GLM-4.7-Flash) ship with
|
||||
tokenizer_class "TokenizersBackend" which breaks GGUF conversion.
|
||||
Modifies the file in-place. Requires model_name to determine the
|
||||
correct replacement class.
|
||||
|
||||
Returns True if a patch was applied.
|
||||
"""
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
fixed_class = _get_tokenizer_class_fix(model_name)
|
||||
if not fixed_class:
|
||||
return False
|
||||
|
||||
config_path = os.path.join(model_dir, "tokenizer_config.json")
|
||||
if not os.path.isfile(config_path):
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
|
||||
tok_class = config.get("tokenizer_class", "")
|
||||
if tok_class == "TokenizersBackend":
|
||||
logger.warning(
|
||||
"Patching tokenizer_class: '%s' → '%s' in %s",
|
||||
tok_class, fixed_class, config_path,
|
||||
)
|
||||
config["tokenizer_class"] = fixed_class
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning("Could not patch tokenizer_config.json: %s", exc)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def patch_tokenizer_in_memory(tokenizer, model_name: str = "") -> bool:
|
||||
"""Fix known broken tokenizer_class on an in-memory tokenizer object.
|
||||
|
||||
Some HF uploads (Qwen3.5, GLM-4.7-Flash) ship with
|
||||
tokenizer_class "TokenizersBackend". Patches init_kwargs so that
|
||||
save_pretrained() writes a corrected tokenizer_config.json.
|
||||
Requires model_name to determine the correct replacement class.
|
||||
|
||||
Returns True if a patch was applied.
|
||||
"""
|
||||
if not model_name:
|
||||
return False
|
||||
|
||||
fixed_class = _get_tokenizer_class_fix(model_name)
|
||||
if not fixed_class:
|
||||
return False
|
||||
|
||||
try:
|
||||
init_kwargs = getattr(tokenizer, "init_kwargs", None) or {}
|
||||
tok_class = init_kwargs.get("tokenizer_class", "")
|
||||
if tok_class == "TokenizersBackend":
|
||||
logger.warning(
|
||||
"Patching in-memory tokenizer_class: '%s' → '%s'",
|
||||
tok_class, fixed_class,
|
||||
)
|
||||
tokenizer.init_kwargs["tokenizer_class"] = fixed_class
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning("Could not patch in-memory tokenizer: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def ensure_transformers_version(model_name: str) -> None:
|
||||
"""Ensure the correct ``transformers`` version is active for *model_name*.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue