fix(security): bound tokenizer work when explicit truncation_side is set (#47007)
Signed-off-by: jperezde <jperezde@redhat.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent
db808b3961
commit
c231d1f290
@@ -64,12 +64,14 @@ class DummyTokenizer:
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._captured_encode_kwargs: dict = {}
|
||||
self._captured_text_len: int = 0
|
||||
|
||||
def decode(self, tokens: list[int]):
|
||||
return str(tokens)
|
||||
|
||||
def encode(self, text: str, **kwargs):
|
||||
self._captured_encode_kwargs = kwargs
|
||||
self._captured_text_len = len(text)
|
||||
|
||||
in_length = len(text)
|
||||
truncation = kwargs.get("truncation")
|
||||
@@ -366,6 +368,85 @@ class TestRenderPrompt:
|
||||
assert results[0]["prompt_token_ids"] == tokens
|
||||
assert results[0]["prompt"] == "[1, 2, 3, 4]"
|
||||
|
||||
def test_explicit_side_tokenizer_unbounded(self):
|
||||
renderer = _build_renderer(MockModelConfig())
|
||||
|
||||
prompts = renderer.render_prompts(
|
||||
_preprocess_prompt(renderer.model_config, "x" * 500)
|
||||
)
|
||||
results = renderer.tokenize_prompts(
|
||||
prompts,
|
||||
TokenizeParams(
|
||||
max_total_tokens=100,
|
||||
truncate_prompt_tokens=4,
|
||||
truncation_side="left",
|
||||
),
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert len(results[0]["prompt_token_ids"]) == 4
|
||||
|
||||
kwargs = renderer.tokenizer._captured_encode_kwargs
|
||||
assert kwargs["truncation"] is False
|
||||
|
||||
def test_explicit_side_left_text(self):
|
||||
renderer = _build_renderer(MockModelConfig())
|
||||
|
||||
prompts = renderer.render_prompts(
|
||||
_preprocess_prompt(renderer.model_config, "x" * 50)
|
||||
)
|
||||
results = renderer.tokenize_prompts(
|
||||
prompts,
|
||||
TokenizeParams(
|
||||
max_total_tokens=100,
|
||||
truncate_prompt_tokens=5,
|
||||
truncation_side="left",
|
||||
),
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert len(results[0]["prompt_token_ids"]) == 5
|
||||
assert results[0]["prompt_token_ids"] == list(range(45, 50))
|
||||
|
||||
def test_explicit_side_right_text(self):
|
||||
renderer = _build_renderer(MockModelConfig())
|
||||
|
||||
prompts = renderer.render_prompts(
|
||||
_preprocess_prompt(renderer.model_config, "x" * 50)
|
||||
)
|
||||
results = renderer.tokenize_prompts(
|
||||
prompts,
|
||||
TokenizeParams(
|
||||
max_total_tokens=100,
|
||||
truncate_prompt_tokens=5,
|
||||
truncation_side="right",
|
||||
),
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert len(results[0]["prompt_token_ids"]) == 5
|
||||
assert results[0]["prompt_token_ids"] == list(range(5))
|
||||
|
||||
def test_explicit_side_text_pretokenization_guard(self):
|
||||
renderer = _build_renderer(MockModelConfig(), max_chars_per_token=1)
|
||||
|
||||
prompts = renderer.render_prompts(
|
||||
_preprocess_prompt(renderer.model_config, "x" * 500)
|
||||
)
|
||||
results = renderer.tokenize_prompts(
|
||||
prompts,
|
||||
TokenizeParams(
|
||||
max_total_tokens=100,
|
||||
truncate_prompt_tokens=4,
|
||||
truncation_side="left",
|
||||
),
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert len(results[0]["prompt_token_ids"]) == 4
|
||||
|
||||
assert renderer.tokenizer._captured_text_len <= 100
|
||||
|
||||
|
||||
class TestRenderEmbedPrompt:
|
||||
def _create_test_embed_bytes(self, tensor: torch.Tensor) -> bytes:
|
||||
|
||||
@@ -314,10 +314,11 @@ class TokenizeParams:
|
||||
# while still failing `self._token_len_check` as expected by users
|
||||
max_length = self.max_input_tokens + 1
|
||||
|
||||
# Explicit truncation-side overrides require the full token sequence so
|
||||
# we can slice from the requested side in _token_truncation. Disable
|
||||
# tokenizer-level truncation because generation tokenizers default to
|
||||
# left truncation while callers may request right truncation.
|
||||
# Explicit truncation-side overrides require the full token sequence
|
||||
# so we can slice from the requested side in _token_truncation.
|
||||
# Disable tokenizer-level truncation because its default side may
|
||||
# differ from the requested side. The defense against unbounded
|
||||
# tokenization lives in _text_len_check (character-level pre-trim).
|
||||
if self.truncation_side is not None and self.truncate_prompt_tokens is not None:
|
||||
return dict(
|
||||
truncation=False,
|
||||
@@ -333,15 +334,13 @@ class TokenizeParams:
|
||||
def _text_len_check(self, tokenizer: TokenizerLike | None, text: str) -> str:
|
||||
"""Apply length checks to prompt text if necessary."""
|
||||
max_input_tokens = self.max_input_tokens
|
||||
if max_input_tokens is None:
|
||||
if max_input_tokens is None or tokenizer is None:
|
||||
return text
|
||||
|
||||
if self.truncate_prompt_tokens is None and tokenizer is not None:
|
||||
max_input_chars = max_input_tokens * tokenizer.max_chars_per_token
|
||||
max_input_chars = max_input_tokens * tokenizer.max_chars_per_token
|
||||
|
||||
if self.truncate_prompt_tokens is None:
|
||||
if len(text) > max_input_chars:
|
||||
# To save resources, fail the request outright without even
|
||||
# attempting tokenization
|
||||
raise VLLMValidationError(
|
||||
f"This model's maximum context length is "
|
||||
f"{self.max_total_tokens} tokens. However, you requested "
|
||||
@@ -354,6 +353,11 @@ class TokenizeParams:
|
||||
parameter="input_text",
|
||||
value=len(text),
|
||||
)
|
||||
elif self.truncation_side is not None and len(text) > max_input_chars:
|
||||
if self.truncation_side == "left":
|
||||
text = text[-max_input_chars:]
|
||||
else:
|
||||
text = text[:max_input_chars]
|
||||
|
||||
return text
|
||||
|
||||
|
||||
Reference in New Issue
Block a user