"""Token counting helpers for inference profiling."""
from __future__ import annotations
import importlib
import logging
from dataclasses import dataclass
from typing import Protocol
logger = logging.getLogger(__name__)
[docs]
@dataclass(frozen=True)
class TokenCount:
"""A token count plus provenance."""
value: int
source: str
exact: bool
[docs]
class TokenCounter(Protocol):
"""Counter interface used by workload generation and fallback accounting."""
source: str
exact: bool
[docs]
def count_text(self, text: str) -> TokenCount:
"""Count tokens in a text payload."""
[docs]
class EstimatedTokenCounter:
"""A deterministic fallback counter based on whitespace-like chunks."""
source = "estimated"
exact = False
[docs]
def count_text(self, text: str) -> TokenCount:
chunks = [chunk for chunk in text.replace("\n", " ").split(" ") if chunk]
return TokenCount(value=max(1, len(chunks)), source=self.source, exact=False)
[docs]
class TiktokenCounter:
"""Token counter backed by tiktoken."""
source = "tiktoken"
exact = True
def __init__(self, *, model: str | None, encoding_name: str | None) -> None:
tiktoken = importlib.import_module("tiktoken")
if encoding_name:
self._encoding = tiktoken.get_encoding(encoding_name)
elif model:
self._encoding = tiktoken.encoding_for_model(model)
else:
self._encoding = tiktoken.get_encoding("cl100k_base")
[docs]
def count_text(self, text: str) -> TokenCount:
return TokenCount(
value=len(self._encoding.encode(text)),
source=self.source,
exact=True,
)
[docs]
def build_token_counter(
*,
tokenizer: str,
model: str,
tokenizer_model: str | None = None,
tiktoken_encoding: str | None = None,
strict: bool = False,
) -> TokenCounter:
"""Build the requested token counter, falling back to estimates in auto mode."""
normalized = tokenizer.strip().lower()
resolved_model = tokenizer_model or model
if normalized in {"none", "estimate", "estimated"}:
return EstimatedTokenCounter()
if normalized == "tiktoken":
return TiktokenCounter(model=resolved_model, encoding_name=tiktoken_encoding)
if normalized in {"transformers", "hf", "huggingface"}:
return TransformersTokenCounter(model=resolved_model)
if normalized != "auto":
raise ValueError(
"--tokenizer must be one of auto, none, tiktoken, transformers"
)
try:
return TiktokenCounter(
model=resolved_model,
encoding_name=tiktoken_encoding,
)
except Exception as exc:
logger.debug("tiktoken unavailable in auto mode: %s", exc)
try:
return TransformersTokenCounter(model=resolved_model)
except Exception as exc:
logger.debug("transformers unavailable in auto mode: %s", exc)
if strict:
raise RuntimeError("No configured tokenizer is available")
return EstimatedTokenCounter()
[docs]
def generate_prompt(target_tokens: int, counter: TokenCounter, *, seed: int) -> str:
"""Generate a deterministic prompt near the requested token count."""
base_words = [
"profile",
"inference",
"latency",
"throughput",
"memory",
"tokens",
"scheduler",
"request",
"streaming",
"capacity",
]
words: list[str] = []
index = seed % len(base_words)
while len(words) < target_tokens * 2:
words.append(base_words[index % len(base_words)])
candidate = " ".join(words)
count = counter.count_text(candidate)
if count.value >= target_tokens:
return candidate
index += 1
return " ".join(words)