Skip to content

normalizers: support Replace and Prepend (fixes gemma-4-31B tokenizer load)#38

Open
itayh-atero wants to merge 4 commits into
crusoecloud:mainfrom
itayh-atero:itayh/prepend-replace-normalizers
Open

normalizers: support Replace and Prepend (fixes gemma-4-31B tokenizer load)#38
itayh-atero wants to merge 4 commits into
crusoecloud:mainfrom
itayh-atero:itayh/prepend-replace-normalizers

Conversation

@itayh-atero

Copy link
Copy Markdown

Adds Replace (literal String + Regex patterns) and Prepend normalizers, with a typed ReplacePattern in the JSON parsing.

@itayh-atero itayh-atero force-pushed the itayh/prepend-replace-normalizers branch 2 times, most recently from 22e4ea2 to c5e28eb Compare June 16, 2026 13:24
…ntered on RedHatAI/gemma-4-31B-it-FP8-Dynamic)
maturin/pyo3 here only supports free-threaded on Python 3.14+, so the
-i python3.13t step failed and aborted the x86_64 job before the wheel
artifact uploaded. We don't need free-threaded wheels (target is regular
CPython 3.12; the cp39-abi3 wheel covers it).
@itayh-atero itayh-atero force-pushed the itayh/prepend-replace-normalizers branch from c5e28eb to 76bd7f8 Compare June 16, 2026 13:34
itayh-atero and others added 2 commits June 16, 2026 17:28
…ecode chain

gemma's tokenizer.json decoder is a Sequence of Replace("▁"->" "),
ByteFallback, Fuse, Strip(" ", 1, 0). DecoderConfig already parsed these,
but the runtime Decoder only implemented ByteLevel/Sequence and stubbed Fuse
as a no-op, so tokenizer construction failed with 'unsupported decoder type:
Replace' (encountered on RedHatAI/gemma-4-31B-it-FP8-Dynamic under vLLM).

- Implement Replace (reusing normalizers::Replace, the inverse direction),
  ByteFallback (run-buffered UTF-8 reassembly; one U+FFFD per byte token on
  an invalid run), real Fuse (concat to one token), and Strip.
- Fix Fuse: was identity; gemma's Strip runs after Fuse and expects a single
  fused token, so a no-op Fuse would strip a leading space from every piece.
- Decoder Replace.pattern: Value -> ReplacePattern to reuse the normalizer's
  pattern engine; re-export normalizers::ReplaceError for the build path.
- Add tests/gemma_replace_decoder.rs: byte-for-byte parity with the HF
  tokenizers crate on the verbatim gemma decoder chain (metaspace, multi-byte
  fallback, emoji, invalid UTF-8, strip-after-fuse, empty input).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PyTokenizer encode hot path held the GIL for the entire Rust
tokenization. Under a high-concurrency serving frontend (e.g. vLLM
tokenizing hundreds of requests at once) this serializes all encodes on
the GIL and blocks the caller's event loop, inflating time-to-first-token
while decode is unaffected. A controlled same-node A/B on gemma-4-31B
(VLLM_USE_FASTOKENS on vs off) showed ~+130% TTFT p50 / -14% throughput
with the GIL held, entirely in the encode/TTFT portion (ITL/TPOT flat).

The read lock was already designed for GIL-released concurrent reads (see
the TokenizerState doc comment), so wrap the tokenization in
py.allow_threads(): acquire the read lock and build the PyEncoding inside
the GIL-released closure, and do only the Py::new allocation under the
GIL. encode_batch gets the same treatment so rayon parallelizes without
pinning the GIL for the whole batch.

(Also: tidy the relocated pad_to_multiple_of math to base.div_ceil(m).)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant