Skip to content

Commit 135238b

Browse files
authored
pad token decoding should use literal decode and not decode which is symantic and may include extra formatting (#54)
1 parent 27234a6 commit 135238b

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ build-backend = "setuptools.build_meta"
2121

2222
[project]
2323
name = "TokeNicer"
24-
version = "0.0.10"
24+
version = "0.0.11"
2525
description = "A (nicer) tokenizer you want to use for model `inference` and `training`: with all known peventable `gotchas` normalized or auto-fixed."
2626
readme = "README.md"
2727
requires-python = ">=3"

tests/test_pad_token.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class TestPadToken(unittest.TestCase):
3030
('/monster/data/model/Llama-3.2-1B-Instruct', '<|finetune_right_pad_id|>'),
3131
('/monster/data/model/Qwen2.5-0.5B-Instruct', '<|fim_pad|>'),
3232
('/monster/data/model/Qwen2-VL-2B-Instruct', '<|endoftext|>'),
33+
('/monster/data/model/ERNIE-4.5-0.3B-PT/', '<unk>', None, True),
3334
('/monster/data/model/gemma-2-9b', '<pad>'),
3435
('/monster/data/model/Hymba-1.5B-Instruct', '<unk>', None, True),
3536
('/monster/data/model/Mistral-7B-Instruct-v0.2', '<unk>'),

tokenicer/tokenicer.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,12 @@ def auto_fix_pad_token(
160160
pad_token_id = getattr(self.tokenizer, "pad_token_id", None)
161161
if pad_token_id is not None:
162162
if getattr(self.tokenizer, "pad_token", None) is None:
163-
self.tokenizer.pad_token = self.tokenizer.decode([pad_token_id])
163+
# Do not round-trip special token ids through decode().
164+
# decode() returns rendered text, not the canonical token literal,
165+
# and some custom tokenizers intentionally inject formatting.
166+
# ERNIE 4.5 is the concrete regression here: decode([0]) returns
167+
# " <unk>" while the real special token is "<unk>".
168+
self.tokenizer.pad_token = self._token_literal_for_id(pad_token_id)
164169
return
165170

166171
if not strict and getattr(self.tokenizer, "eos_token_id", None) is not None:
@@ -208,7 +213,10 @@ def auto_fix_pad_token(
208213
)
209214

210215
self.tokenizer.pad_token_id = pad_token_id
211-
self.tokenizer.pad_token = self.tokenizer.decode([pad_token_id])
216+
# Preserve the canonical special-token spelling instead of using
217+
# decode([pad_token_id]). For tokenizers like ERNIE 4.5, decode() is a
218+
# detokenization API and may prepend whitespace to special tokens.
219+
self.tokenizer.pad_token = self._token_literal_for_id(pad_token_id)
212220

213221
if getattr(model_config, "pad_token_id", None) is None:
214222
# Keep the resolved text config aligned so downstream callers do
@@ -217,6 +225,39 @@ def auto_fix_pad_token(
217225

218226
logger.info(f"Tokenicer: Auto fixed pad_token_id={pad_token_id} (token='{self.tokenizer.pad_token}').")
219227

228+
def _token_literal_for_id(self, token_id: int) -> str:
229+
# Prefer declared special-token metadata over decode(). The metadata is
230+
# the canonical token literal used by save/load, while decode() is a
231+
# text-rendering path and may be lossy for standalone special tokens.
232+
special_token_attrs = (
233+
"pad_token",
234+
"unk_token",
235+
"eos_token",
236+
"bos_token",
237+
"cls_token",
238+
"sep_token",
239+
"mask_token",
240+
)
241+
242+
for attr in special_token_attrs:
243+
attr_id = getattr(self.tokenizer, f"{attr}_id", None)
244+
attr_value = getattr(self.tokenizer, attr, None)
245+
if attr_id == token_id and attr_value is not None:
246+
return attr_value
247+
248+
special_tokens_map = getattr(self.tokenizer, "special_tokens_map", {}) or {}
249+
for attr_value in special_tokens_map.values():
250+
if isinstance(attr_value, list):
251+
for item in attr_value:
252+
if isinstance(item, str) and self.tokenizer.convert_tokens_to_ids(item) == token_id:
253+
return item
254+
elif isinstance(attr_value, str) and self.tokenizer.convert_tokens_to_ids(attr_value) == token_id:
255+
return attr_value
256+
257+
# Final fallback for tokenizers that do not expose enough special-token
258+
# metadata to recover the literal directly.
259+
return self.tokenizer.decode([token_id])
260+
220261
@staticmethod
221262
def _resolve_text_model_config(model_config, seen=None):
222263
if model_config is None:

0 commit comments

Comments
 (0)