OpenVINO Tokenizers: Incorporate Text Processing Into OpenVINO Pipelines#
This Jupyter notebook can be launched on-line, opening an interactive environment in a browser window. You can also make a local installation. Choose one of the following options:
OpenVINO Tokenizers is an OpenVINO extension and a Python library designed to streamline tokenizer conversion for seamless integration into your projects. It supports Python and C++ environments and is compatible with all major platforms: Linux, Windows, and MacOS.
Table of contents:
Tokenization Basics#
One does not simply put text into a neural network, only numbers. The process of transforming text into a sequence of numbers is called tokenization. It usually contains several steps that transform the original string, splitting it into parts - tokens - with an associated number in a dictionary. You can check the interactive GPT-4 tokenizer to gain an intuitive understanding of the principles of tokenizer work.
There are two important points in the tokenizer-model relation: 1. Every neural network with text input is paired with a tokenizer and cannot be used without it. 2. To reproduce the model’s accuracy on a specific task, it is essential to utilize the same tokenizer employed during the model training.
That is why almost all model repositories on HuggingFace
Hub also contain tokenizer files
(tokenizer.json
, vocab.txt
, merges.txt
, etc.).
The process of transforming a sequence of numbers into a string is called detokenization. Detokenizer can share the token dictionary with a tokenizer, like any LLM chat model, or operate with an entirely distinct dictionary. For instance, translation models dealing with different source and target languages often necessitate separate dictionaries.
Some tasks only need a tokenizer, like text classification, named entity recognition, question answering, and feature extraction. On the other hand, for tasks such as text generation, chat, translation, and abstractive summarization, both a tokenizer and a detokenizer are required.
This is a self-contained example that relies solely on its own code.
We recommend running the notebook in a virtual environment. You only need a Jupyter server to start. For details, please refer to Installation Guide.
Acquiring OpenVINO Tokenizers#
OpenVINO Tokenizers Python library allows you to convert HuggingFace
tokenizers into OpenVINO models. To install all required dependencies
use pip install openvino-tokenizers[transformers]
.
%pip install -Uq pip
%pip install -q -U "openvino>=2024.3.0" openvino-tokenizers[transformers] openvino-genai
%pip install "numpy<2.0.0" "torch>=2.1" --extra-index-url https://download.pytorch.org/whl/cpu
from pathlib import Path
tokenizer_dir = Path("tokenizer/")
model_id = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T"
Convert Tokenizer from HuggingFace Hub with CLI Tool#
The first way is to use the CLI utility, bundled with OpenVINO
Tokenizers. Use --with-detokenizer
flag to add the detokenizer model
to the output. By setting --clean-up-tokenization-spaces=False
we
ensure that the detokenizer correctly decodes a code-generation model
output. --trust-remote-code
flag works the same way as passing
trust_remote_code=True
to AutoTokenizer.from_pretrained
constructor.
!convert_tokenizer $model_id --with-detokenizer -o $tokenizer_dir
Loading Huggingface Tokenizer... Converting Huggingface Tokenizer to OpenVINO... Saved OpenVINO Tokenizer: tokenizer/openvino_tokenizer.xml, tokenizer/openvino_tokenizer.bin Saved OpenVINO Detokenizer: tokenizer/openvino_detokenizer.xml, tokenizer/openvino_detokenizer.bin ⚠️ If you have any problems with the command above on MacOS, try to install tbb.
The result is two OpenVINO models: openvino_tokenizer
and
openvino_detokenizer
. Both can be interacted with using
read_model
, compile_model
and save_model
, similar to any
other OpenVINO model.
Convert Tokenizer from HuggingFace Hub with Python API#
The other method is to pass HuggingFace hf_tokenizer
object to
convert_tokenizer
function:
from transformers import AutoTokenizer
from openvino_tokenizers import convert_tokenizer
hf_tokenizer = AutoTokenizer.from_pretrained(model_id)
ov_tokenizer, ov_detokenizer = convert_tokenizer(hf_tokenizer, with_detokenizer=True)
ov_tokenizer, ov_detokenizer
(<Model: 'tokenizer'
inputs[
<ConstOutput: names[string_input] shape[?] type: string>
]
outputs[
<ConstOutput: names[input_ids] shape[?,?] type: i64>,
<ConstOutput: names[attention_mask] shape[?,?] type: i64>
]>,
<Model: 'detokenizer'
inputs[
<ConstOutput: names[Parameter_22] shape[?,?] type: i64>
]
outputs[
<ConstOutput: names[string_output] shape[?] type: string>
]>)
That way you get OpenVINO model objects. Use save_model
function
from OpenVINO to reuse converted tokenizers later:
⚠️ Import
openvino_tokenizers
will add all tokenizer-related operations to OpenVINO, after which you can work with saved tokenizers and detokenizers.
import openvino as ov
# This import is needed to add all tokenizer-related operations to OpenVINO
import openvino_tokenizers # noqa: F401
ov.save_model(ov_tokenizer, tokenizer_dir / "openvino_tokenizer.xml")
ov.save_model(ov_detokenizer, tokenizer_dir / "openvino_detokenizer.xml")
To use the tokenizer, compile the converted model and input a list of strings. It’s essential to be aware that not all original tokenizers support multiple strings (also called batches) as input. This limitation arises from the requirement for all resulting number sequences to maintain the same length. To address this, a padding token must be specified, which will be appended to shorter tokenized strings. In cases where no padding token is determined in the original tokenizer, OpenVINO Tokenizers defaults to using \(0\) for padding. Presently, only right-side padding is supported, typically used for classification tasks, but not suitable for text generation.
tokenizer, detokenizer = ov.compile_model(ov_tokenizer), ov.compile_model(ov_detokenizer)
test_strings = ["Test", "strings"]
token_ids = tokenizer(test_strings)["input_ids"]
print(f"Token ids: {token_ids}")
detokenized_text = detokenizer(token_ids)["string_output"]
print(f"Detokenized text: {detokenized_text}")
Token ids: [[ 1 4321]
[ 1 6031]]
Detokenized text: ['Test' 'strings']
We can compare the result of converted (de)tokenizer with the original one:
hf_token_ids = hf_tokenizer(test_strings).input_ids
print(f"Token ids: {hf_token_ids}")
hf_detokenized_text = hf_tokenizer.batch_decode(hf_token_ids)
print(f"Detokenized text: {hf_detokenized_text}")
Token ids: [[1, 4321], [1, 6031]]
Detokenized text: ['<s> Test', '<s> strings']
Text Generation Pipeline with OpenVINO Tokenizers#
Let’s build a text generation pipeline with OpenVINO Tokenizers and minimal dependencies. To obtain an OpenVINO model we will use the Optimum library. The latest version allows you to get a so-called stateful model.
The original TinyLlama-1.1B-intermediate-step-1431k-3T
model is
4.4Gb. To reduce network and disk usage we will load a converted model
which has also been compressed to int8
. The original conversion
command is commented.
model_dir = Path(Path(model_id).name)
if not model_dir.exists():
# Converting the original model
# %pip install -U "git+https://github.com/huggingface/optimum-intel.git" "nncf>=2.8.0" onnx
# %optimum-cli export openvino -m $model_id --task text-generation-with-past $model_dir
# Load already converted model
from huggingface_hub import hf_hub_download
hf_hub_download(
"chgk13/TinyLlama-1.1B-intermediate-step-1431k-3T",
filename="openvino_model.xml",
local_dir=model_dir,
)
hf_hub_download(
"chgk13/TinyLlama-1.1B-intermediate-step-1431k-3T",
filename="openvino_model.bin",
local_dir=model_dir,
)
import numpy as np
from tqdm.notebook import trange
from pathlib import Path
from openvino_tokenizers.constants import EOS_TOKEN_ID_NAME
core = ov.Core()
ov_model = core.read_model(model_dir / "openvino_model.xml")
compiled_model = core.compile_model(ov_model)
infer_request = compiled_model.create_infer_request()
The infer_request
object provides control over the model’s state - a
Key-Value cache that speeds up inference by reducing computations.
Multiple inference requests can be created, and each request maintains a
distinct and separate state.
text_input = ["Quick brown fox jumped"]
model_input = {name.any_name: output for name, output in tokenizer(text_input).items()}
if "position_ids" in (input.any_name for input in infer_request.model_inputs):
model_input["position_ids"] = np.arange(model_input["input_ids"].shape[1], dtype=np.int64)[np.newaxis, :]
# No beam search, set idx to 0
model_input["beam_idx"] = np.array([0], dtype=np.int32)
# End of sentence token is that model signifies the end of text generation
# Read EOS token ID from rt_info of tokenizer/detokenizer ov.Model object
eos_token = ov_tokenizer.get_rt_info(EOS_TOKEN_ID_NAME).value
tokens_result = np.array([[]], dtype=np.int64)
# Reset KV cache inside the model before inference
infer_request.reset_state()
max_infer = 5
for _ in trange(max_infer):
infer_request.start_async(model_input)
infer_request.wait()
output_tensor = infer_request.get_output_tensor()
# Get the most probable token
token_indices = np.argmax(output_tensor.data, axis=-1)
output_token = token_indices[:, -1:]
# Concatenate previous tokens result with newly generated token
tokens_result = np.hstack((tokens_result, output_token))
if output_token[0, 0] == eos_token:
break
# Prepare input for the next inference iteration
model_input["input_ids"] = output_token
model_input["attention_mask"] = np.hstack((model_input["attention_mask"].data, [[1]]))
model_input["position_ids"] = np.hstack(
(
model_input["position_ids"].data,
[[model_input["position_ids"].data.shape[-1]]],
)
)
text_result = detokenizer(tokens_result)["string_output"]
print(f"Prompt:\n{text_input[0]}")
print(f"Generated:\n{text_result[0]}")
0%| | 0/5 [00:00<?, ?it/s]
Prompt:
Quick brown fox jumped
Generated:
over the fence.
Text Generation Pipeline with OpenVINO GenAI and OpenVINO Tokenizers#
OpenVINO GenAI is a flavor of OpenVINO, aiming to simplify running inference of generative AI models. It hides the complexity of the generation process and minimizes the amount of code required. OpenVINO GenAI depends on OpenVINO and OpenVINO Tokenizers.
Firstly we need to create a pipeline with LLMPipeline
.
LLMPipeline
is the main object used for text generation using LLM in
OpenVINO GenAI API. You can construct it straight away from the folder
where both converted model and tokenizer are located,
e.g. ov_genai.LLMPipeline(model_and_tokenizer_path)
.
As the model and tokenizer are located in different directories, we
create a ov_genai.Tokenizer
object by providing the path to saved
tokenizer. Then we will provide directory with model, tokenizer object
and device for LLMPipeline
. Lastly we run generate
method and
get the output in text format.
Additionally, we can configure parameters for decoding. We can get the
default config with get_generation_config()
, setup parameters, and
apply the updated version with set_generation_config(config)
or put
config directly to generate()
. It’s also possible to specify the
needed options just as inputs in the generate()
method, as shown
below, e.g. we can add max_new_tokens
to stop generation if a
specified number of tokens is generated and the end of generation is not
reached.
Let’s build the same text generation pipeline, but with simplified Python OpenVINO Generate API. We will use the same model and tokenizer downloaded in previous steps.
import openvino_genai as ov_genai
genai_tokenizer = ov_genai.Tokenizer(str(tokenizer_dir))
pipe = ov_genai.LLMPipeline(str(model_dir), genai_tokenizer, "CPU")
result = pipe.generate(text_input[0], max_new_tokens=max_infer)
print(f"Prompt:\n{text_input[0]}")
print(f"Generated:\n{result}")
Prompt:
Quick brown fox jumped
Generated:
over the lazy dog.
Merge Tokenizer into a Model#
Packages like tensorflow-text
offer the convenience of integrating
text processing directly into the model, streamlining both distribution
and usage. Similarly, with OpenVINO Tokenizers, you can create models
that combine a converted tokenizer and a model. It’s important to note
that not all scenarios benefit from this merge. In cases where a
tokenizer is used once and a model is inferred multiple times, as seen
in the earlier text generation example, maintaining a separate
(de)tokenizer and model is advisable to prevent unnecessary
tokenization-detokenization cycles during inference. Conversely, if both
a tokenizer and a model are used once in each pipeline inference,
merging simplifies the workflow and aids in avoiding the creation of
intermediate objects:
The OpenVINO Python API allows you to avoid this by using the
share_inputs
option during inference, but it requires additional
input from a developer every time the model is inferred. Combining the
models and tokenizers simplifies memory management. Moreover, after the
combining models inputs have changed - original model has three inputs
(input_ids
, attention_mask
, token_type_ids
) and combined
model has only one input for text input prompt.
model_id = "mrm8488/bert-tiny-finetuned-sms-spam-detection"
model_dir = Path(Path(model_id).name)
if not model_dir.exists():
%pip install -qU git+https://github.com/huggingface/optimum-intel.git "onnx<1.16.2"
!optimum-cli export openvino --model $model_id --task text-classification $model_dir
from openvino_tokenizers import connect_models
core = ov.Core()
text_input = ["Free money!!!"]
ov_tokenizer = core.read_model(model_dir / "openvino_tokenizer.xml")
ov_model = core.read_model(model_dir / "openvino_model.xml")
combined_model = connect_models(ov_tokenizer, ov_model)
ov.save_model(combined_model, model_dir / "combined_openvino_model.xml")
print("Original OpenVINO model inputs:")
for input in ov_model.inputs:
print(input)
print("\nCombined OpenVINO model inputs:")
for input in combined_model.inputs:
print(input)
compiled_combined_model = core.compile_model(combined_model)
openvino_output = compiled_combined_model(text_input)
print(f"\nLogits: {openvino_output['logits']}")
Original OpenVINO model inputs:
<Output: names[input_ids] shape[?,?] type: i64>
<Output: names[attention_mask] shape[?,?] type: i64>
<Output: names[token_type_ids] shape[?,?] type: i64>
Combined OpenVINO model inputs:
<Output: names[Parameter_4430] shape[?] type: string>
Logits: [[ 1.2007061 -1.469803 ]]
Conclusion#
The OpenVINO Tokenizers integrate text processing operations into the OpenVINO ecosystem. Enabling the conversion of HuggingFace tokenizers into OpenVINO models, the library allows efficient deployment of deep learning pipelines across varied environments. The feature of combining tokenizers and models not only simplifies memory management but also helps to streamline model usage and deployment.