mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-10 00:08:58 +00:00
Fix Qwen3 NaN loss: delegate pad_token repair to shared unsloth_zoo.pad_token (#6524)
* Fix Qwen3 NaN: self-heal vision pad_token in load_correct_tokenizer Text-only Qwen3 (and Qwen2.5) models share Qwen3-VL's vocab, so their Hub tokenizer configs ship <|vision_pad|> as pad_token. Padding text-only training with a vision token corrupts attention/loss and produces NaN losses and gradients on affected stacks. patch_tokenizer already heals this, but only when a model with config is passed. The standalone load_correct_tokenizer path (and custom training loops) still returned <|vision_pad|>. This adds a model-independent guard in load_correct_tokenizer that replaces a vision pad_token on text-only tokenizers with the first safe text token (<|endoftext|>, <pad>, [PAD], <unk>), falling back to eos_token only if it differs from pad_token. The result now matches upstream Qwen configs (pad_token <|endoftext|>, id 151643) with no new token added. Vision processors (image_processor present) and non-vision pad tokens (Llama, Qwen2) are left untouched. Fixes #3155 * Format pad_token helper for ruff kwarg-spacing hook (pre-commit) * Harden vision pad_token fix: drop unk candidate, guard get_vocab, skip vision eos * Tighten code comments (no logic change) * Delegate pad_token fix to shared unsloth_zoo.pad_token Generalize the narrow _fix_vision_pad_token by delegating to unsloth_zoo's shared fix_pad_token (single source of truth, AGPL-3.0), which scans the reserved-token families instead of only the vision-pad case. A guarded import keeps this working against an older unsloth_zoo that has not shipped the module yet: on ImportError it falls back to _fix_vision_pad_token. allow_add=False is passed so the early load_correct_tokenizer call stays side-effect free (no model here to resize embeddings); the later model-aware patch_tokenizer call finishes the job and is idempotent. Adds tests/python/test_pad_token_fix.py covering both dispatch paths offline.
This commit is contained in:
parent
a963ec92e7
commit
b91cdc8793
2 changed files with 185 additions and 0 deletions
112
tests/python/test_pad_token_fix.py
Normal file
112
tests/python/test_pad_token_fix.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"""_fix_pad_token dispatch in unsloth/tokenizer_utils.py.
|
||||
|
||||
It must delegate to unsloth_zoo's shared fix_pad_token when present (single
|
||||
source of truth), and fall back to the narrow vision-token swap against an
|
||||
older unsloth_zoo. Static + CPU-only: the two helpers are exec'd in isolation
|
||||
so the test never imports torch / transformers / unsloth.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
TOK_PATH = os.path.join(REPO_ROOT, "unsloth", "tokenizer_utils.py")
|
||||
|
||||
WANTED = {
|
||||
"_VISION_PAD_TOKENS",
|
||||
"_SAFE_TEXT_PAD_TOKENS",
|
||||
"_fix_vision_pad_token",
|
||||
"_fix_pad_token",
|
||||
}
|
||||
|
||||
|
||||
def _load_pad_helpers():
|
||||
"""Exec only the pad-token helpers with a stub logger (no heavy imports)."""
|
||||
tree = ast.parse(open(TOK_PATH).read())
|
||||
nodes = []
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
names = {t.id for t in node.targets if isinstance(t, ast.Name)}
|
||||
if names & WANTED:
|
||||
nodes.append(node)
|
||||
elif isinstance(node, ast.FunctionDef) and node.name in WANTED:
|
||||
nodes.append(node)
|
||||
module = ast.Module(body = nodes, type_ignores = [])
|
||||
ast.fix_missing_locations(module)
|
||||
ns = {"logger": types.SimpleNamespace(warning = lambda *a, **k: None)}
|
||||
exec(compile(module, TOK_PATH, "exec"), ns)
|
||||
return ns
|
||||
|
||||
|
||||
class FakeTok:
|
||||
def __init__(self, vocab, pad, eos):
|
||||
self._v = dict(vocab)
|
||||
self.pad_token = pad
|
||||
self.eos_token = eos
|
||||
|
||||
@property
|
||||
def pad_token_id(self):
|
||||
return self._v.get(self.pad_token)
|
||||
|
||||
@property
|
||||
def eos_token_id(self):
|
||||
return self._v.get(self.eos_token)
|
||||
|
||||
def get_vocab(self):
|
||||
return dict(self._v)
|
||||
|
||||
|
||||
def _block_shared_module(monkeypatch):
|
||||
# Stub parent so importing it is cheap, and mark the submodule absent.
|
||||
monkeypatch.setitem(sys.modules, "unsloth_zoo", types.ModuleType("unsloth_zoo"))
|
||||
monkeypatch.setitem(sys.modules, "unsloth_zoo.pad_token", None)
|
||||
|
||||
|
||||
def test_fix_pad_token_none_is_noop():
|
||||
ns = _load_pad_helpers()
|
||||
assert ns["_fix_pad_token"](None) is None
|
||||
|
||||
|
||||
def test_fix_pad_token_falls_back_without_shared_module(monkeypatch):
|
||||
ns = _load_pad_helpers()
|
||||
_block_shared_module(monkeypatch)
|
||||
# Qwen3 text tokenizer shipping a vision pad_token -> narrow swap heals it.
|
||||
tok = FakeTok(
|
||||
{"<|endoftext|>": 1, "<|im_end|>": 2, "<|vision_pad|>": 3},
|
||||
pad = "<|vision_pad|>",
|
||||
eos = "<|im_end|>",
|
||||
)
|
||||
ns["_fix_pad_token"](tok)
|
||||
assert tok.pad_token == "<|endoftext|>"
|
||||
assert tok.pad_token != tok.eos_token
|
||||
|
||||
|
||||
def test_fallback_leaves_valid_pad_untouched(monkeypatch):
|
||||
ns = _load_pad_helpers()
|
||||
_block_shared_module(monkeypatch)
|
||||
tok = FakeTok({"<s>": 1, "</s>": 2, "<pad>": 0}, pad = "<pad>", eos = "</s>")
|
||||
ns["_fix_pad_token"](tok)
|
||||
assert tok.pad_token == "<pad>"
|
||||
|
||||
|
||||
def test_fix_pad_token_delegates_to_shared_module(monkeypatch):
|
||||
ns = _load_pad_helpers()
|
||||
calls = {}
|
||||
|
||||
fake = types.ModuleType("unsloth_zoo.pad_token")
|
||||
|
||||
def fix_pad_token(tokenizer, allow_add = True):
|
||||
calls["allow_add"] = allow_add
|
||||
tokenizer.pad_token = "SHARED"
|
||||
|
||||
fake.fix_pad_token = fix_pad_token
|
||||
monkeypatch.setitem(sys.modules, "unsloth_zoo", types.ModuleType("unsloth_zoo"))
|
||||
monkeypatch.setitem(sys.modules, "unsloth_zoo.pad_token", fake)
|
||||
|
||||
tok = FakeTok({"a": 1}, pad = "x", eos = "y")
|
||||
ns["_fix_pad_token"](tok)
|
||||
# Delegated, and crucially with allow_add=False (no model here to resize).
|
||||
assert tok.pad_token == "SHARED"
|
||||
assert calls["allow_add"] is False
|
||||
Loading…
Add table
Add a link
Reference in a new issue