Skip to main content

tokenizer-fix-python

Fix Python conversion issues in OpenVINO Tokenizers. Use when: tokenizer-diagnostics reports root_cause_location=python, unsupported types need new handlers in hf_parser.py, pipeline step mapping or merging is incorrect, tokenizer_pipeline.py step classes need fixes.

ソース情報

リポジトリ
openvinotoolkit/openvino_tokenizers
ソースの最終更新活動
2026年4月1日 11:33
検出された SKILL.md の言語
英語
スター
57
フォーク
64

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
tokenizer-fix-python
description
Fix Python conversion issues in OpenVINO Tokenizers. Use when: tokenizer-diagnostics reports root_cause_location=python, unsupported types need new handlers in hf_parser.py, pipeline step mapping or merging is incorrect, tokenizer_pipeline.py step classes need fixes.
argument-hint
model_id and diagnosis details (e.g. unsupported_types: [NewType], affected_stages: [pre_tokenizer])
# OpenVINO Tokenizer Python Fixer Fixes issues in the Python conversion layer that maps HuggingFace tokenizer pipelines to OpenVINO Tokenizers. ## When to Use - `tokenizer-diagnostics` reported `root_cause_location: python` - Common issue categories: - **Unsupported type**: A HuggingFace step type is missing from the parser maps - **Incorrect mapping**: A supported type produces wrong OV steps (wrong params, missing steps) - **Merge bug**: `merge_regex_split_steps` incorrectly combines incompatible `RegexSplitStep` instances during `finalize()` - **Missing pipeline step class**: A new OV operation needs a Python step wrapper ## Inputs **Required:** - **model_id**: HuggingFace model identifier - **diagnosis**: Output from the `tokenizer-diagnostics` skill, particularly: - `unsupported_types` — which HF types need handlers - `affected_stages` — which pipeline stages diverge - `description` — human-readable root cause summary **Helpful context (from tokenizer-diagnostics output):** - The HF→OV pipeline mapping (Step 2 output) - Finalized pipeline showing step merges - Pre-tokenization expected vs actual token splits ## Prerequisites Activate the Python virtual environment before running any commands. 1. **Locate the virtual environment** — check for common directories at the repository root: `.venv/`, `venv/`, `env/`. Use `list_dir` to find it. If none is found, ask the user for its location. 2. **Activate** based on the current platform: - **Linux/macOS**: `source <venv_path>/bin/activate` - **Windows (cmd)**: `<venv_path>\Scripts\activate.bat` - **Windows (PowerShell)**: `<venv_path>\Scripts\Activate.ps1` ## Architecture Overview ### Conversion Pipeline ``` HuggingFace tokenizer │ ▼ TransformersTokenizerPipelineParser (hf_parser.py) │ reads tokenizer.json │ maps each section via type→handler dicts ▼ TokenizerPipeline (tokenizer_pipeline.py) │ list of *Step dataclasses │ finalize() → merges steps, transforms vocab ▼ OV Model (via get_tokenizer_ov_subgraph) │ each Step.get_ov_subgraph() creates C++ ops ▼ Compiled OpenVINO model ``` ### Key Files | File | Purpose | |------|---------| | `python/openvino_tokenizers/hf_parser.py` | Parser with type→handler maps for all pipeline sections | | `python/openvino_tokenizers/tokenizer_pipeline.py` | Pipeline step dataclasses, `TokenizerPipeline`, `finalize()` | | `python/openvino_tokenizers/convert_tokenizer.py` | Entry point: dispatches to fast/sentencepiece/tiktoken converters | | `src/ov_extension.cpp` | C++ operation registration (lists all available ops) | ### Parser Maps (hf_parser.py) The parser uses four type→handler dictionaries. Each handler takes a `step_dict` (from `tokenizer.json`) and returns one or more `*Step` instances: | Map | Pipeline Section | Approx Line | |-----|-----------------|-------------| | `normalizers_map` | `tokenizer.json["normalizer"]` | ~L194 | | `pre_tokenization_map` | `tokenizer.json["pre_tokenizer"]` | ~L229 | | `post_tokenization_map` | `tokenizer.json["post_processor"]` | ~L279 | | `decoding_map` | `tokenizer.json["decoder"]` | ~L367 | The tokenization model (BPE/WordPiece/Unigram/WordLevel) is handled by the `tokenization_model()` method (~L265), not via a map. ### Step Class Hierarchy (tokenizer_pipeline.py) ``` BasePipelineStep ├── NormalizationStep (normalizer ops) │ ├── RegexNormalizationStep │ ├── NormalizeUnicode │ ├── CaseFoldStep │ └── CharsmapStep ├── PreTokenizatinStep (splitting ops) │ ├── RegexSplitStep │ ├── WhitespaceSplitStep │ └── BytesToCharsStep ├── TokenizationModelStep (vocabulary lookup) │ ├── BPETokenizationStep │ ├── WordPieceTokenizationStep │ ├── UnigramModelStep │ └── VocabEncoderStep ├── PostTokenizationStep (combine/truncate/pad) │ ├── CombineSegmentsStep │ ├── TruncationStep │ └── PaddingStep └── DecodingStep (detokenizer ops) ├── VocabDecoderStep ├── CharsToBytesStep ├── FuseStep └── ByteFallbackStep ``` ### Available C++ Operations (src/ov_extension.cpp) RegexNormalization, RegexSplit, BPETokenizer, WordpieceTokenizer, BytesToChars, CharsToBytes, CombineSegments, VocabEncoder, VocabDecoder, TrieTokenizer, Truncate, FuzeRagged, ByteFallback, SpecialTokensSplit, CharsMapNormalization, CaseFold, NormalizeUnicode, UnigramTokenizer, UTF8Validate, SentencepieceTokenizer, SentencepieceDetokenizer, SentencepieceStreamDetokenizer, RaggedToDense, RaggedToSparse, RaggedToRagged, StringToHashBucket, StringTensorPack, StringTensorUnpack, RaggedTensorPack, EqualStr ## Procedure ### Step 1: Extract the tokenizer.json structure Download and inspect the HF tokenizer's pipeline definition: ```python from tokenizers import Tokenizer import json tok = Tokenizer.from_pretrained("<model_id>") tj = json.loads(tok.to_str()) # Inspect the failing section print(json.dumps(tj["pre_tokenizer"], indent=2)) # or normalizer, decoder, etc. ``` Alternatively, use `openvino_tokenizers diagnose <model_id>` output which already maps all sections. ### Step 2: Identify the fix category Based on the diagnosis, determine which category the fix falls into: #### Category A: Missing type in parser map **Symptom**: `unsupported_types: [SomeType]` in diagnosis output. **Fix pattern**: 1. Read the HF tokenizers source or `tokenizer.json` spec to understand what `SomeType` does 2. Find or create the corresponding OV step class in `tokenizer_pipeline.py` 3. Add the handler to the appropriate map in `hf_parser.py`: ```python # In hf_parser.py — add to the appropriate map: normalizers_map["NewType"] = lambda step_dict: SomeStep( param1=step_dict.get("param1", "default"), param2=step_dict["param2"], ) ``` If a new step class is needed: ```python # In tokenizer_pipeline.py — add a new step: @dataclass class NewNormalizationStep(NormalizationStep): param1: str = "" def get_ov_subgraph(self, input_nodes: list[Output]) -> list[Output]: # Use an existing C++ op input_nodes.extend(create_string_constant_node(self.param1)) return ( _get_factory() .create("RegexNormalization", input_nodes, {"global_replace": True}) .outputs() ) ``` The `get_ov_subgraph` pattern: 1. Extend `input_nodes` with constant parameters (strings via `create_string_constant_node`, scalars via `make_constant_node`) 2. Call `_get_factory().create("<OpName>", input_nodes, {attributes_dict})` to create the OV C++ operation 3. Return `.outputs()` — the list of output nodes #### Category B: Incorrect parameter parsing **Symptom**: Conversion succeeds but outputs differ. Diagnosis shows no unsupported types but pre-tokenization or full pipeline tests fail. **Fix pattern**: 1. Read the HF `tokenizer.json` for the failing step and compare with the parser handler 2. Common issues: - Reading wrong key from `step_dict` (e.g. `"String"` vs `"Regex"` in pattern dicts) - Missing boolean flags that affect behavior - Default values that don't match HF defaults Example — how handler reads a pattern dict: ```python # HF tokenizer.json may have: {"pattern": {"String": "abc"}} or {"pattern": {"Regex": "a.*c"}} pattern = step_dict["pattern"].get("String") or step_dict["pattern"]["Regex"] ``` #### Category C: Pipeline merge bug (merge_regex_split_steps) **Symptom**: Diagnosis shows `⚠ Pre-tokenization merge: N steps → M steps` and pre-tokenization test fails. The merged regex pattern in the finalized pipeline produces different splits than running the original patterns sequentially. **Where**: `TokenizerPipeline.merge_regex_split_steps()` in `tokenizer_pipeline.py` (~L1498). **How it works**: The method iterates over `RegexSplitStep` instances and tries to combine them via `RegexSplitStep.__add__`, which joins patterns with `|` (OR). It merges when `invert`, `behaviour`, and `max_splits` all match. **Common fix approaches**: 1. **Prevent merge for incompatible patterns**: Add a check in `RegexSplitStep.__add__` that raises `ValueError` when patterns can't be safely merged (the merge loop catches `ValueError` and keeps steps separate): ```python def __add__(self, other: "RegexSplitStep") -> "RegexSplitStep": # ... existing checks ... # Example: prevent merging patterns with conflicting quantifiers if _patterns_conflict(self.split_pattern, other.split_pattern): raise ValueError("Patterns cannot be safely merged") return self.__class__( split_pattern="|".join((self.split_pattern, other.split_pattern)), ... ) ``` 2. **Skip merge entirely for specific step configurations**: Add a `mergeable` flag or check specific pattern signatures that are known to conflict. 3. **Fix the merge logic**: If the `|`-join is semantically wrong for certain pattern combinations, the patterns may need wrapping in non-capturing groups `(?:pattern1)|(?:pattern2)` or the merge should be skipped. #### Category D: Step finalization issue **Symptom**: Individual step mapping looks correct but the finalized pipeline differs unexpectedly. **Where**: `TokenizerPipeline.finalize()` in `tokenizer_pipeline.py` (~L1547) and individual `Step.finalize()` methods. **Key finalization behaviors to be aware of**: - `BPETokenizationStep.finalize()` (~L665): Removes `BytesToCharsStep` and `CharsToBytesStep` from the pipeline when `is_byte_level=True`, absorbing byte-level encoding into the BPE vocab - `merge_regex_split_steps()` (~L1498): Merges compatible `RegexSplitStep` instances - `del_duplicated_split_steps()`: Removes duplicate whitespace splitters - `update_metaspace_step_with_special_tokens()`: Patches metaspace regex when special tokens exist ### Step 3: Implement the fix 1. Read the relevant source files to understand the current implementation 2. Apply the minimal fix — do not refactor surrounding code 3. Ensure the fix handles edge cases present in the test strings: - Whitespace variants: `\t`, `\n`, multiple spaces - Empty strings - Unicode: emoji, CJK, accented characters, RTL - Mixed content: digits + letters + punctuation ### Step 4: Verify After applying the fix, rebuild and test: ```bash # Rebuild (triggers CMake for C++ extensions) pip install --pre -Ue . --extra-index-url https://storage.openvinotoolkit.org/simple/wheels/nightly # Run targeted check openvino_tokenizers check <model_id> [flags] # Run diagnose to verify pre-tokenization now matches openvino_tokenizers diagnose <model_id> [flags] # Run unit tests for the affected operation python -m pytest tests/layer_tests.py -k <relevant_test> -v # Run integration tests for the model type python -m pytest tests/tokenizers_test.py -k <relevant_test> -v # Run the full test suite to check for regressions python -m pytest tests/ -v ``` ### Step 5: Check for regressions The fix must not break other tokenizers. Key model sets to verify: - **WordPiece models**: `bert-base-multilingual-cased`, `google/mobilebert-uncased` - **BPE models**: `openai-community/gpt2`, `roberta-base`, `NousResearch/Meta-Llama-3-8B-Instruct` - **SentencePiece models**: `NousResearch/Llama-2-13b-hf`, `microsoft/Phi-3-mini-128k-instruct` - **TikToken models**: `Qwen/Qwen-14B-Chat` At minimum, run `openvino_tokenizers check` against 2-3 models of the same type as the fixed tokenizer. ## Common Pitfalls 1. **Don't forget `parse()`**: `TransformersTokenizerPipelineParser.__init__` does NOT populate the pipeline — you must call `parser.parse()` first. 2. **Handler return types**: Map handlers must return either a single `*Step` instance or a `list[*Step]`. The parser wraps singles in lists internally. 3. **Regex dialect**: OV uses PCRE2 regex (via the C++ `RegexSplit` / `RegexNormalization` ops). HF's Rust tokenizers uses the `fancy-regex` crate. Key differences: Unicode property classes (`\p{L}`, `\p{N}`) are supported in both, but lookahead/lookbehind support may differ. 4. **Byte-level encoding**: `ByteLevel` pre-tokenizer in HF does TWO things: regex split AND byte→char mapping. In OV these are separate steps: `RegexSplitStep` + `BytesToCharsStep`. The `BytesToCharsStep` is later removed during `BPETokenizationStep.finalize()` which absorbs it into the vocab. 5. **Step ordering matters**: Pre-tokenization steps are applied sequentially. `merge_regex_split_steps` combines them into fewer ops for efficiency, but this changes the execution from sequential to parallel `|`-alternation. ## Security - **NEVER** run arbitrary code from `tokenizer.json` — only read data fields - **NEVER** install packages — assume the environment is pre-configured - **NEVER** modify `model_id` — pass it exactly as provided - Validate that regex patterns from `tokenizer.json` are used only via the safe OV regex API (no `eval()`, no `subprocess`)
GitHubで見る