Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/CLI_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ Flag names shown as `--full.dotted.path --alias`. Both forms work.
- `--model-params.max-new-tokens --max-output-tokens` - Max output tokens (default: 1024)
- `--model-params.osl-distribution.min --min-output-tokens` - Min output tokens (default: 1)
- `--model-params.streaming --streaming` - Streaming mode: auto/on/off (default: auto)
- `--model-params.tokenizer-name --tokenizer` - HF repo ID or local tokenizer path used for client-side ISL/OSL/TPOT; overrides the served model name
- `--model-params.enable-token-metrics / --model-params.no-enable-token-metrics` - Enable or disable client-side ISL/OSL/TPOT (default: enabled). Disabling skips tokenizer discovery and loading; non-token metrics are still collected.
- `--runtime.min-duration-ms --duration` - Min duration: ms default, or with suffix (600s, 10m) (default: 600000)
- `--runtime.n-samples-to-issue --num-samples` - Explicit sample count override
- `--client.num-workers --workers` - HTTP workers (-1=auto, default: -1)
Expand All @@ -118,6 +120,30 @@ Flag names shown as `--full.dotted.path --alias`. Both forms work.

**All other schema fields** are accessible via dotted paths (e.g., `--model-params.temperature`, `--model-params.top-k`, `--runtime.scheduler-random-seed`). Run `--help` to see the full list.

### Tokenizer selection and Kimi K3

By default, the benchmark uses `model_params.name` to find the tokenizer. Set
`tokenizer_name` when the endpoint exposes a served alias or when the tokenizer
is stored at a different path on the benchmark host:

```yaml
model_params:
name: "kimi-k3" # Name sent to the endpoint.
tokenizer_name: "moonshotai/Kimi-K3" # HF repo or benchmark-host path.
```

Kimi K3's custom Transformers tokenizer is supported even though Transformers
labels it as a slow tokenizer: its BPE core uses the optimized Rust `tiktoken`
backend. This framework capability does not by itself add Kimi K3 to an MLPerf
ruleset's list of official submission models.

If token metrics are not needed, avoid tokenizer discovery and loading with:

```yaml
model_params:
enable_token_metrics: false
```

## Environment Variables

**In YAML files** — use `${VAR}` or `${VAR:-default}` syntax:
Expand Down
25 changes: 16 additions & 9 deletions docs/async_utils/services/metrics_aggregator/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,21 @@ counted in `pending`. `flush_remaining` never raises.
### Sharded batch encoding (`BatchTokenizer`)

The drain fans the whole buffer out across worker **processes**, one pinned
per `CORES_PER_WORKER` (8) core block. Each worker runs the raw `tokenizers`
backend's `encode_batch_fast` (Rust, rayon); a single BPE rayon pool
saturates ~8 cores, so disjoint pinned blocks are how the whole machine is
used. Workers are spawn-context, warmed in parallel at construction (bounded
— a hung load is a startup error), and ignore SIGINT.
per `CORES_PER_WORKER` (8) core block. Hugging Face Fast tokenizers run the raw
`tokenizers` backend's `encode_batch_fast` (Rust, rayon). Custom Transformers
wrappers backed by `tiktoken.Encoding`, including Kimi K3, run their native
`encode` method through a length-counting adapter. Keeping the wrapper in the
path preserves its special-token and long-input handling; process sharding
provides parallelism when the wrapper has no batch API. A single BPE rayon pool
saturates ~8 cores, so disjoint pinned blocks are how the whole machine is used.
Workers are spawn-context, warmed in parallel at construction (bounded — a hung
load is a startup error), and ignore SIGINT.

The shard pool has no knob: it auto-sizes to one shard per 8-core block of
the allowed CPU universe. There is no fallback — no fast Rust backend, or a
failed/over-budget warmup, is a startup error, because an in-process slow
path cannot keep up and would surface much later as an incomplete drain.
the allowed CPU universe. There is no fallback — no supported optimized
backend (Hugging Face Fast or `tiktoken`), or a failed/over-budget warmup, is a
startup error, because an in-process slow path cannot keep up and would surface
much later as an incomplete drain.
Platforms without an affinity API (macOS) shard unpinned; each worker caps
its rayon pool to the block size instead.

Expand All @@ -71,7 +76,9 @@ SIGTERM-kills the aggregator child — which the aggregator's SIGTERM handler
turns into a best-effort `INTERRUPTED` final snapshot.

Chat-template items (tool calls) run on the in-process thread lane —
`apply_chat_template` is Python/Jinja; sharding buys nothing.
`apply_chat_template` is Python/Jinja; sharding buys nothing. They use the
tokenizer's native `tokenize=True` path so model-specific segment policies,
including Kimi K3's XTML special-token boundaries, are preserved.

### CPU affinity: tokenize is post-run

Expand Down
2 changes: 1 addition & 1 deletion examples/10_Agentic_Inference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ sglang serve \
--mem-fraction-static 0.95
```

`--model-path` is the checkpoint loaded by the server. It can be a local path visible to the server container or a Hugging Face model ID, depending on your SGLang environment. `--served-model-name` is the OpenAI model name exposed to clients; set `model_params.name` in the YAML to the same value.
`--model-path` is the checkpoint loaded by the server. It can be a local path visible to the server container or a Hugging Face model ID, depending on your SGLang environment. `--served-model-name` is the OpenAI model name exposed to clients; set `model_params.name` in the YAML to the same value. If that served name is not also a loadable tokenizer on the benchmark host, set `model_params.tokenizer_name` to the tokenizer's Hugging Face repo or benchmark-host path so client-side ISL/OSL/TPOT metrics are collected.

## Client YAML

Expand Down
6 changes: 5 additions & 1 deletion examples/10_Agentic_Inference/kimi_agentic_benchmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ version: "1.0"
type: "online"

model_params:
name: "/model"
name: "kimi-k2.6"
# The served name above need not be a loadable tokenizer. Point this at the
# Kimi tokenizer's HF repo or a local path visible on the benchmark host so
# client-side ISL/OSL/TPOT metrics are collected.
tokenizer_name: "/path/to/Kimi-K2.6"
temperature: 1.0
top_p: 0.95
max_new_tokens: 8192
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ dependencies = [
"rich==14.3.3",
# Needed for tokenization and OSL reporting
"transformers==5.5.0",
# Required by custom model tokenizers such as Kimi K3. Although exposed as
# a Transformers "slow" tokenizer, K3 delegates BPE to this Rust backend.
"tiktoken==0.13.0",
# Required by transformers' apply_chat_template
"jinja2==3.1.6",
"numpy>=1.26.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@
per-sample text. The sharded pool is the drain-phase accelerator and is
auto-sized (one shard per core block); live mid-run flushes run on a small
in-process thread pool (``--tokenizer-workers``, default 2) owned by the
queue's live loop. A tokenizer without a fast (Rust) backend is a startup
error, never a silent slow path. Platforms without CPU affinity (e.g. macOS)
shard unpinned at full speed; only cache/NUMA locality is lost.
queue's live loop. A tokenizer without a supported optimized backend (a Hugging
Face Fast ``tokenizers`` backend or a Rust ``tiktoken.Encoding`` core) is a
startup error, never a silent slow path. Platforms without CPU affinity (e.g.
macOS) shard unpinned at full speed; only cache/NUMA locality is lost.
"""

from __future__ import annotations

import asyncio
import inspect
import json
import logging
import multiprocessing
Expand All @@ -38,7 +40,7 @@
from collections.abc import Callable
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from itertools import chain
from typing import TYPE_CHECKING, Any, Protocol, cast
from typing import TYPE_CHECKING, Any, Protocol

import msgspec
from inference_endpoint.endpoint_client.cpu_affinity import (
Expand Down Expand Up @@ -122,14 +124,64 @@ def load_reference_tokenizer(tokenizer_name: str) -> Any:
return AutoTokenizer.from_pretrained(tokenizer_name, trust_remote_code=True)


class _TikTokenBackend:
"""Length-counting adapter for a Transformers wrapper around tiktoken.

Hugging Face labels custom tokenizers such as Kimi K3's
``TikTokenTokenizer`` as "slow" because they inherit
``PreTrainedTokenizer`` and do not expose ``backend_tokenizer``. Their BPE
core is nevertheless the Rust implementation in ``tiktoken.Encoding``.

Keep the model-provided wrapper in the loop instead of calling
``Encoding.encode_batch`` directly. Kimi K3's ``encode`` method preserves
its special-token policy and splits very long or pathological strings to
avoid tiktoken's input-size and long-whitespace-run failure modes. The
outer Endpoints process sharding supplies parallelism across texts.
"""

def __init__(self, tokenizer: Any) -> None:
self._tokenizer = tokenizer
# Kimi K3 exposes this explicit switch so raw user/model text cannot
# reinterpret a literal ``<|...|>`` substring as an XTML control token.
# Other tiktoken-backed wrappers may not expose it.
try:
self._supports_allow_special = (
"allow_special_tokens" in inspect.signature(tokenizer.encode).parameters
)
except (TypeError, ValueError):
self._supports_allow_special = False

def count_texts(self, texts: list[str]) -> list[int]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude + Code-Quality] medium (performance): _TikTokenBackend.count_texts tokenizes a batch with a serial Python loop of single-string encode calls ([len(self._tokenizer.encode(text, allow_special_tokens=False)) for text in texts]). This backend runs on both the live in-process lane and inside each drain shard. The HF path (line 223-224) instead issues one encode_batch(...) rayon call that parallelizes across the whole pinned core block. Shards pin CORES_PER_WORKER = 8 cores and export RAYON_NUM_THREADS=8 on the assumption one encode call saturates ~8 cores — but single-string tiktoken encode is single-threaded, so each 8-core shard runs on ~1 core (roughly 8x per-shard throughput loss) plus per-text GIL overhead on the 1024-item live flushes. The docstring claims sharding "supplies parallelism across texts," but that's cross-process; the 8 cores within each block sit idle for tiktoken. This weakens the very "tokenizer keeps up with completions" guarantee that makes a slow backend a startup error.

Root cause: the two count_texts branches are also a copy-pasted comprehension differing only by one kwarg (DRY). The durable fix addresses both: factor the loop once (kwargs = {"allow_special_tokens": False} if self._supports_allow_special else {}), and either batch the encode or adjust the shard core-block sizing to reflect that tiktoken shards are effectively single-core, so allocated CPUs aren't wasted.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against moonshotai/Kimi-K3 revision 9f62e4e9fffbd0a83ddd60e1c209d828994b3569. For 256 ordinary ~12k-character texts, the wrapper loop took 0.111s versus 0.0177s for tiktoken.Encoding.encode_batch(..., num_threads=8), with identical counts—approximately 6.3x in this single warm-process comparison.

One important caveat: replacing the wrapper loop with raw full-string encode_batch is not token-count equivalent for long inputs. For a 450,003-character input, the Kimi wrapper returned 150,003 tokens while raw encoding returned 150,002 because the wrapper applies its 400k outer split and 25k consecutive-run split.

Suggested fix: reproduce Kimi’s exact safe chunk boundaries, batch the resulting chunks with disallowed_special=(), retain an owner index for each chunk, and sum chunk lengths back into each original text. This preserves the PR’s allow_special_tokens=False policy while using all assigned cores.

Please add parity tests for ordinary text, literal special-token text, the 25k/400k boundaries, and over-boundary inputs, plus a small throughput regression benchmark.

if self._supports_allow_special:
return [
len(self._tokenizer.encode(text, allow_special_tokens=False))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude] low (data-integrity): the tiktoken text path forces allow_special_tokens=False, so a special-token literal (e.g. <|...|>) in a prompt or output is counted as many literal-text tokens. The HF fast path (encode_batch(..., add_special_tokens=False), 223-224) and the chat-template path (_chat_template_token_count, tokenize=True) both keep registered special tokens recognized as one token. Result: for identical text with special-token literals, a tiktoken-backed model (Kimi K3) reports a different ISL/OSL than an HF-backed model, and diverges from the server's real count — a silent, tokenizer-family-dependent metric shift. Edge case (models rarely emit raw special-token strings) and allow_special=False is defensible for untrusted text, but document the intentional asymmetry or align the two text paths.

for text in texts
]
return [len(self._tokenizer.encode(text)) for text in texts]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Codex + Claude] medium (data-integrity): this fallback branch calls encode(text) with the wrapper's defaults — for a tiktoken-backed wrapper that inherits PreTrainedTokenizerBase.encode, that means add_special_tokens=True (verified against pinned transformers 5.5.0; e.g. a Llama-family encode("count my tokens please") gives 5 vs 4 with add_special_tokens=False), so every text is over-counted by its BOS/EOS frame.

Every other counting path in this module is deliberately no-specials: the HF fast path uses encode_batch(texts, add_special_tokens=False) (line 224) and the in-proc fallback uses tokenize() (line 431). Result: systematic ISL/OSL/TPOT inflation for exactly the "other tiktoken-backed wrappers" case this branch exists to support (Kimi K3 exposes allow_special_tokens and takes the first branch, so K3 itself is unaffected).

Fix: probe add_special_tokens in __init__ the same way allow_special_tokens is probed and build one kwargs dict — which also collapses the two duplicated comprehensions flagged in the earlier DRY comment:

# __init__
params = inspect.signature(tokenizer.encode).parameters
self._encode_kwargs = {}
if "allow_special_tokens" in params:
    self._encode_kwargs["allow_special_tokens"] = False
if "add_special_tokens" in params:
    self._encode_kwargs["add_special_tokens"] = False

# count_texts
return [len(self._tokenizer.encode(t, **self._encode_kwargs)) for t in texts]



def _backend_from_tokenizer(tokenizer: Any) -> Any | None:
"""Return a supported optimized length-counting backend."""
backend = getattr(tokenizer, "backend_tokenizer", None)
if backend is not None:
return backend

model = getattr(tokenizer, "model", None)
model_module = type(model).__module__.partition(".")[0]
if model_module == "tiktoken" and callable(getattr(tokenizer, "encode", None)):
return _TikTokenBackend(tokenizer)
return None


def load_reference_backend(tokenizer_name: str) -> Any | None:
"""Raw tokenizers backend (fast Rust path) for length counting.
"""Optimized backend for reference-tokenizer length counting.

Counting through the backend avoids the transformers "sequence longer than
model_max_length" warning the Python wrapper emits, so no ``model_max_length``
override is needed. ``None`` if the tokenizer has no fast backend.
Usually this is the raw Hugging Face ``tokenizers`` backend. A custom
Transformers tokenizer backed by ``tiktoken.Encoding`` receives an adapter
that preserves its model-specific encode behavior. ``None`` means there is
no supported accelerated backend.
"""
return getattr(load_reference_tokenizer(tokenizer_name), "backend_tokenizer", None)
return _backend_from_tokenizer(load_reference_tokenizer(tokenizer_name))


def _init_worker(tokenizer_name: str, core_set: list[int]) -> None:
Expand Down Expand Up @@ -160,15 +212,34 @@ def _init_worker(tokenizer_name: str, core_set: list[int]) -> None:
global _WORKER_BACKEND
_WORKER_BACKEND = load_reference_backend(tokenizer_name)
if _WORKER_BACKEND is not None:
_WORKER_BACKEND.encode("warmup", add_special_tokens=False)
encode_lengths(_WORKER_BACKEND, ["warmup"])


def encode_lengths(backend: Any, texts: list[str]) -> list[int]:
"""Per-text token counts via the raw tokenizers backend, one rayon call."""
"""Per-text counts through an optimized HF Fast or tiktoken backend."""
count_texts = getattr(backend, "count_texts", None)
if count_texts is not None:
return count_texts(texts)
encode_batch = getattr(backend, "encode_batch_fast", None) or backend.encode_batch
return [len(e.ids) for e in encode_batch(texts, add_special_tokens=False)]


def _chat_template_token_count(tokenizer: Any, messages: list[dict[str, Any]]) -> int:
"""Count a rendered conversation through the tokenizer's native path.

In particular, Kimi K3 assigns ``allow_special`` per XTML segment. Rendering
to one string and subsequently calling ``tokenize`` loses those boundaries
and can reinterpret literal special-token text from users or tools as
structure. ``tokenize=True`` preserves the model tokenizer's exact policy.
"""
encoded = tokenizer.apply_chat_template(
messages, tokenize=True, add_generation_prompt=False
)
if isinstance(encoded, dict):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude, orchestrator-verified] critical (bug): apply_chat_template(..., tokenize=True) in the pinned transformers 5.5.0 defaults return_dict=True and returns a BatchEncoding — a UserDict, not a dict subclass — so this isinstance(encoded, dict) never fires and len(encoded) returns the mapping's key count (2: input_ids + attention_mask).

Reproduced end-to-end against the pinned install with this exact function and a real chat tokenizer (TinyLlama-1.1B-Chat): the function returns 2 where the true token count is 28 (the pre-PR tokenize=False + tok.tokenize(rendered) path also gives 28, so this is a regression).

Downstream, no exception is raised so nothing falls back: _load_tokenizer computes _prefix_len = 2, with_assistant = 2_baseline = 0, and _token_count_message gets full = 2max(0, 2 - 2 - 0) = 0. Every assistant/tool-call message silently records 0 tokens — for every tokenizer, not just Kimi K3.

The unit test (test_chat_template_uses_native_tokenize_path) passes only because _FakeTokenizerWithTemplate.apply_chat_template returns a plain list for tokenize=True, so the real transformers return contract is never exercised.

Fix: pass return_dict=False explicitly (returns the flat id list and skips building the attention mask), or replace the guard with isinstance(encoded, Mapping). Either way, make the fake return a BatchEncoding-shaped mapping so the actual contract is pinned by the test.

encoded = encoded["input_ids"]
return len(encoded)


def _worker_encode_lengths(texts: list[str]) -> list[int]:
"""Per-text token counts for a shard, in one rayon-parallel call."""
backend = _WORKER_BACKEND
Expand Down Expand Up @@ -248,6 +319,7 @@ def __init__(
max_workers=max(1, live_workers), thread_name_prefix="tok-thread"
)
self._load_tokenizer() # also computes the chat-template baseline
self._backend = _backend_from_tokenizer(self._tokenizer)
# Process shards for the batched text path. Empty only when
# in-process mode was explicitly requested (n_workers=0 or
# cores_per_worker<=0; ctor overrides used primarily by tests —
Expand All @@ -263,22 +335,11 @@ def _load_tokenizer(self) -> None:
# Baseline = tokens from a [user, empty-assistant] pair minus the [user]
# prefix alone, so the assistant frame is subtracted from message counts.
try:
prefix = cast(
str,
tok.apply_chat_template(
[_PREFIX_USER_MSG], tokenize=False, add_generation_prompt=False
),
self._prefix_len = _chat_template_token_count(tok, [_PREFIX_USER_MSG])
with_assistant = _chat_template_token_count(
tok, [_PREFIX_USER_MSG, {"role": "assistant", "content": ""}]
)
self._prefix_len = len(tok.tokenize(prefix))
with_assistant = cast(
str,
tok.apply_chat_template(
[_PREFIX_USER_MSG, {"role": "assistant", "content": ""}],
tokenize=False,
add_generation_prompt=False,
),
)
self._baseline = len(tok.tokenize(with_assistant)) - self._prefix_len
self._baseline = with_assistant - self._prefix_len
except Exception:
self._prefix_len = 0
self._baseline = 0
Expand All @@ -296,18 +357,19 @@ def _setup_shards(self, cores_per_worker: int, n_workers: int) -> None:
process's affinity mask (or the online CPU count when the platform
has no affinity API — shards then run unpinned), always at least one;
an explicit count is clamped to that capacity. An environment that
cannot shard — no fast Rust backend, a warmup that fails or exceeds
its budget — raises instead of silently degrading to a slow path
that cannot keep up with completions.
cannot shard — no optimized backend, a warmup that fails or exceeds
its budget — raises instead of silently degrading to a slow path that
cannot keep up with completions.
"""
if cores_per_worker <= 0 or n_workers == 0:
logger.info("BatchTokenizer: in-process tokenization (explicit)")
return
if getattr(self._tokenizer, "backend_tokenizer", None) is None:
if self._backend is None:
raise RuntimeError(
f"tokenizer {self._tokenizer_name!r} has no fast (Rust) "
"backend; token metrics require one to keep up with "
"completions. Use a fast tokenizer, or disable token metrics."
f"tokenizer {self._tokenizer_name!r} has no supported optimized "
"backend (Hugging Face Fast or tiktoken); token metrics require "
"one to keep up with completions. Use a supported tokenizer, "
"add a backend adapter, or disable token metrics."
)
# The full allowed CPU universe (cgroup-clamped) drives the shard block
# math. cgroup_clamped_cpus owns the probe-and-restore of this process's
Expand Down Expand Up @@ -364,11 +426,9 @@ def _setup_shards(self, cores_per_worker: int, n_workers: int) -> None:
# -- batched text path --------------------------------------------------

def _encode_lengths_inproc(self, texts: list[str]) -> list[int]:
tok = self._tokenizer
backend = getattr(tok, "backend_tokenizer", None)
if backend is not None:
return encode_lengths(backend, texts)
return [len(tok.tokenize(t)) for t in texts] # type: ignore[union-attr]
if self._backend is not None:
return encode_lengths(self._backend, texts)
return [len(self._tokenizer.tokenize(t)) for t in texts] # type: ignore[union-attr]

async def count_texts_async(
self,
Expand Down Expand Up @@ -424,10 +484,9 @@ def _token_count_message(
if tool_calls:
msg["tool_calls"] = _normalize_tool_calls_for_template(tool_calls)
try:
rendered = tok.apply_chat_template( # type: ignore[union-attr]
[_PREFIX_USER_MSG, msg], tokenize=False, add_generation_prompt=False
full = _chat_template_token_count( # type: ignore[arg-type]
tok, [_PREFIX_USER_MSG, msg]
)
full = len(tok.tokenize(rendered)) # type: ignore[union-attr]
return max(0, full - self._prefix_len - self._baseline)
except Exception as exc:
key = f"{self._tokenizer_name}:{type(exc).__name__}"
Expand Down
17 changes: 9 additions & 8 deletions src/inference_endpoint/commands/benchmark/accuracy.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def _load_osl_backend(has_accuracy: bool, tokenizer_name: str | None) -> Any | N
"""Load the reference tokenizer backend for accuracy OSL, or None to disable.

Loaded only when a real accuracy dataset exists; a load failure or a tokenizer
with no fast (Rust) backend disables OSL rather than failing scoring.
with no supported optimized backend disables OSL rather than failing scoring.
"""
if not (has_accuracy and tokenizer_name is not None):
return None
Expand All @@ -202,15 +202,16 @@ def _load_osl_backend(has_accuracy: bool, tokenizer_name: str | None) -> Any | N
e,
)
return None
# A tokenizer with no fast (Rust) backend disables OSL rather than falling
# back to a slow Python-tokenizer count: the perf side
# (token_metrics._setup_shards) requires a fast backend too and raises without
# one, so OSL stays fast-only and consistent on both sides. Warn so the skip is
# visible instead of silently dropping the block.
# A tokenizer with no supported optimized backend disables OSL rather than
# falling back to a slow Python-tokenizer count: the perf side
# (token_metrics._setup_shards) has the same requirement and raises without
# one, so OSL stays consistent on both sides. Warn so the skip is visible
# instead of silently dropping the block.
if osl_backend is None:
logger.warning(
"Accuracy OSL disabled: tokenizer %r has no fast (Rust) backend "
"(token counting requires one, as on the perf side)",
"Accuracy OSL disabled: tokenizer %r has no supported optimized "
"backend (Hugging Face Fast or tiktoken; token counting requires "
"one, as on the perf side)",
tokenizer_name,
)
return osl_backend
Expand Down
5 changes: 4 additions & 1 deletion src/inference_endpoint/commands/benchmark/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,10 @@ def setup_benchmark(
model_name = config.model_params.name
tokenizer_override = config.model_params.tokenizer_name
tokenizer_name: str | None
if tokenizer_override:
if not config.model_params.enable_token_metrics:
logger.info("Client-side token metrics disabled by configuration")
tokenizer_name = None
elif tokenizer_override:
if not _check_tokenizer_exists(tokenizer_override):
raise SetupError(
f"Tokenizer override '{tokenizer_override}' could not be verified. "
Expand Down
Loading
Loading