Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
30 changes: 27 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ keywords = ["llm", "genai", "inference", "nvidia", "distributed"]
dynamo-runtime = { path = "lib/runtime", version = "1.4.0" }
dynamo-llm = { path = "lib/llm", version = "1.4.0" }
dynamo-rl = { path = "lib/rl", version = "1.4.0" }
dynamo-tokenizers = { version = "=1.5.4" }
dynamo-tokenizers = { version = "=1.6.0" }
dynamo-renderer = { version = "=4.0.0" }
dynamo-tokens = { path = "lib/tokens", version = "1.4.0" }
dynamo-truthy = { path = "lib/truthy", version = "1.4.0" }
Expand Down
11 changes: 6 additions & 5 deletions components/src/dynamo/frontend/frontend_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class FrontendConfig(RouterConfigBase, KvRouterConfigBase, AicPerfConfigBase):
trust_remote_code: bool
frontend_route_extensions: list[str]

_VALID_TOKENIZER_BACKENDS = {"default", "fastokens"}
_VALID_TOKENIZER_BACKENDS = {"default", "fastokens", "basetenkenizer"}

def validate(self) -> None:
if self.load_aware:
Expand Down Expand Up @@ -505,11 +505,12 @@ def add_arguments(self, parser) -> None:
default="default",
dest="tokenizer_backend",
help=(
"Tokenizer backend for BPE models: 'default' (HuggingFace tokenizers library) "
"or 'fastokens' (fastokens crate for high-performance BPE encoding). "
"Decoding always uses HuggingFace. Has no effect on TikToken models."
"Tokenizer backend for BPE models: 'default' (HuggingFace tokenizers library), "
"'fastokens' (fastokens crate for high-performance BPE encoding), or "
"'basetenkenizer' (Baseten Tokenizer for native encoding and decoding). "
"Has no effect on TikToken models."
),
choices=["default", "fastokens"],
choices=["default", "fastokens", "basetenkenizer"],
)

add_negatable_bool_argument(
Expand Down
29 changes: 20 additions & 9 deletions docs/fern/components/frontend/Tokenizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: Tokenizer
subtitle: Selects between the default HuggingFace and fastokens tokenizer backends for BPE models served through the Dynamo Frontend.
subtitle: Selects the HuggingFace, fastokens, or Baseten tokenizer backend for BPE models served through the Dynamo Frontend.
---

The Dynamo Frontend supports multiple tokenizer backends for BPE-based `tokenizer.json` models. `BPE` is the underlying tokenization algorithm, not a backend-specific feature: both the default HuggingFace path and the `fastokens` path can serve these models. The backend choice controls which implementation performs tokenization before requests are sent to the inference engine.
The Dynamo Frontend supports multiple tokenizer backends for BPE-based `tokenizer.json` models. `BPE` is the underlying tokenization algorithm, not a backend-specific feature: the default HuggingFace, `fastokens`, and `basetenkenizer` paths can all serve supported BPE models. The backend choice controls which implementation performs tokenization before requests are sent to the inference engine.

## Tokenizer Backends

Expand All @@ -21,10 +21,17 @@ It is a _hybrid_ backend: encoding uses `fastokens` while decoding falls back to

Use this backend when tokenization is a measurable bottleneck, for example on high-concurrency prefill-heavy workloads.

#### `basetenkenizer` Native Encoder and Decoder

The `basetenkenizer` backend uses the Baseten Tokenizer implementation exposed by `dynamo-tokenizers`, a high-performance Rust BPE implementation for inference. Unlike the hybrid `fastokens` path, it performs both encoding and decoding natively and supports segmented encoding for renderers that must preserve trusted control-token boundaries.

Use this backend for supported `tokenizer.json` models when you need Baseten Tokenizer behavior, including token-compatible Kimi tokenizer artifacts.

#### Compatibility notes:

- Works with standard BPE `tokenizer.json` files (Qwen, LLaMA, GPT-family, Mistral, DeepSeek, etc.).
- If `fastokens` cannot load a particular tokenizer file, the frontend logs a warning and transparently falls back to HuggingFace; requests are never dropped.
- If `fastokens` or `basetenkenizer` cannot load a particular tokenizer file, the frontend logs a warning and transparently falls back to HuggingFace; requests are never dropped.
- Special tokens declared only in a sibling `tokenizer_config.json` are preserved for Baseten encoding and decoding and for Dynamo's L1 prefix-cache boundaries.
- Has no effect on TikToken-format tokenizers (`.model` / `.tiktoken` files), which always use the TikToken backend.

## Configuration
Expand All @@ -33,7 +40,7 @@ Set the backend with a CLI flag or environment variable. The CLI flag takes prec

| CLI Argument | Env Var | Valid values | Default |
|---|---|---|---|
| `--tokenizer` | `DYN_TOKENIZER` | `default`, `fastokens` | `default` |
| `--tokenizer` | `DYN_TOKENIZER` | `default`, `fastokens`, `basetenkenizer` | `default` |

**Examples:**

Expand All @@ -44,13 +51,17 @@ python -m dynamo.frontend --tokenizer fastokens
# Environment variable
export DYN_TOKENIZER=fastokens
python -m dynamo.frontend

# Baseten Tokenizer
python -m dynamo.frontend --tokenizer basetenkenizer
```

## Dynamo Frontend Behavior

When `DYN_TOKENIZER=fastokens` is set:
When a non-default backend is selected:

1. The frontend passes the environment variable to the Rust runtime.
2. When building the tokenizer for a model, `ModelDeploymentCard::tokenizer()` attempts to load `fastokens::Tokenizer` from the same `tokenizer.json` file.
3. If loading succeeds, a hybrid `FastTokenizer` is created that encodes with `fastokens` and decodes with HuggingFace.
4. If loading fails (unsupported tokenizer features, missing file, etc.), the frontend logs a warning and falls back to the standard HuggingFace backend; no operator intervention is needed.
1. The frontend resolves `--tokenizer` / `DYN_TOKENIZER` and passes the selected backend to the Rust runtime.
2. `ModelDeploymentCard::tokenizer()` loads the HuggingFace tokenizer first for fallback behavior and L1 cache special-token metadata.
3. Dynamo constructs `FastTokenizer` for `fastokens` or `BasetenTokenizer` for `basetenkenizer` from the same `tokenizer.json` file.
4. If construction fails because the tokenizer uses unsupported features, Dynamo logs a warning and falls back to HuggingFace.
5. When the L1 prefix cache is enabled, Dynamo wraps the selected backend with the same special-token boundary metadata and cache metrics used by the default path.
4 changes: 2 additions & 2 deletions docs/fern/components/frontend/frontend-config-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -492,9 +492,9 @@ See the [Frontend Guide](frontend-guide.md) for KServe message formats, gRPC tun
## Tokenizer

<ParamField path="--tokenizer" type="string" default="default">
Tokenizer implementation. `default` uses HuggingFace; `fastokens` uses the high-performance Rust tokenizer. See [Tokenizer](Tokenizer.md).
Tokenizer implementation. `default` uses HuggingFace, `fastokens` uses the high-performance hybrid encoder, and `basetenkenizer` uses Baseten Tokenizer for native encoding and decoding. See [Tokenizer](Tokenizer.md).

<span className="enum-values"><span className="enum-label">Allowed values:</span> <Badge intent="note" minimal>default</Badge> <Badge intent="note" minimal>fastokens</Badge></span>
<span className="enum-values"><span className="enum-label">Allowed values:</span> <Badge intent="note" minimal>default</Badge> <Badge intent="note" minimal>fastokens</Badge> <Badge intent="note" minimal>basetenkenizer</Badge></span>

Environment variable: `DYN_TOKENIZER`
</ParamField>
Expand Down
4 changes: 2 additions & 2 deletions docs/fern/features/tokenizer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,11 @@ For any new model, validate on representative prompts before rolling out broadly
</Accordion>

<Accordion title="Why do the logs show an unrecognized DYN_TOKENIZER value?">
Use only `fastokens` or `default` for `DYN_TOKENIZER`. Values such as `fast`, `hf`, or `huggingface` are benchmark-runner aliases, not valid values for the frontend environment variable.
Use `default`, `fastokens`, or `basetenkenizer` for `DYN_TOKENIZER`. Values such as `fast`, `hf`, or `huggingface` are benchmark-runner aliases, not valid values for the frontend environment variable.
</Accordion>

<Accordion title="What happens when the model uses .model or .tiktoken files?">
The `fastokens` setting has no effect for TikToken-format tokenizers. Dynamo uses the existing TikToken backend, so you should not expect the `Using fastokens tokenizer backend` log or a `fastokens` speedup.
The `fastokens` and `basetenkenizer` settings have no effect for TikToken-format tokenizers. Dynamo uses the existing TikToken backend, so you should not expect either alternate-backend activation log or speedup.
</Accordion>

<Accordion title="Why doesn't TTFT improve?">
Expand Down
28 changes: 26 additions & 2 deletions lib/bindings/python/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 53 additions & 10 deletions lib/llm/src/local_model/runtime_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ pub const VLLM_INFERENCE_V1_GENERATE_CAPABILITY: &str = "vllm_inference_v1_gener
pub enum TokenizerBackend {
Default,
Fastokens,
Basetenkenizer,
}

impl TokenizerBackend {
pub fn as_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::Fastokens => "fastokens",
Self::Basetenkenizer => "basetenkenizer",
}
}

Expand All @@ -79,11 +81,12 @@ impl TokenizerBackend {
pub fn from_env_or_default() -> Self {
match std::env::var(ENV_TOKENIZER_BACKEND) {
Ok(v) if v == "fastokens" => Self::Fastokens,
Ok(v) if v == "basetenkenizer" => Self::Basetenkenizer,
Ok(v) if v == "default" || v.is_empty() => Self::Default,
Ok(v) => {
tracing::warn!(
value = %v,
"Unrecognized DYN_TOKENIZER value, expected 'fastokens' or 'default'; falling back to default"
"Unrecognized DYN_TOKENIZER value, expected 'default', 'fastokens', or 'basetenkenizer'; falling back to default"
);
Self::Default
}
Expand All @@ -99,8 +102,9 @@ impl FromStr for TokenizerBackend {
match value {
"default" => Ok(Self::Default),
"fastokens" => Ok(Self::Fastokens),
"basetenkenizer" => Ok(Self::Basetenkenizer),
_ => Err(format!(
"invalid tokenizer backend '{value}' (expected 'default' or 'fastokens')"
"invalid tokenizer backend '{value}' (expected 'default', 'fastokens', or 'basetenkenizer')"
)),
}
}
Expand Down Expand Up @@ -683,6 +687,14 @@ mod tests {
);
});

temp_env::with_vars([(ENV_TOKENIZER_BACKEND, Some("basetenkenizer"))], || {
let cfg = ModelRuntimeConfig::default();
assert_eq!(
cfg.effective_tokenizer_backend(),
TokenizerBackend::Basetenkenizer
);
});

temp_env::with_vars([(ENV_TOKENIZER_BACKEND, Some("default"))], || {
let cfg = ModelRuntimeConfig::default();
assert_eq!(cfg.effective_tokenizer_backend(), TokenizerBackend::Default);
Expand Down Expand Up @@ -715,18 +727,49 @@ mod tests {
TokenizerBackend::Fastokens
);
});

temp_env::with_vars([(ENV_TOKENIZER_BACKEND, Some("fastokens"))], || {
let cfg = ModelRuntimeConfig {
tokenizer_backend: Some(TokenizerBackend::Basetenkenizer),
..Default::default()
};
assert_eq!(
cfg.effective_tokenizer_backend(),
TokenizerBackend::Basetenkenizer
);
});
}

#[test]
fn tokenizer_backend_roundtrips_through_serde_json() {
let cfg = ModelRuntimeConfig {
tokenizer_backend: Some(TokenizerBackend::Fastokens),
..Default::default()
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("\"tokenizer_backend\":\"fastokens\""));
let parsed: ModelRuntimeConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.tokenizer_backend, Some(TokenizerBackend::Fastokens));
for backend in [
TokenizerBackend::Default,
TokenizerBackend::Fastokens,
TokenizerBackend::Basetenkenizer,
] {
let cfg = ModelRuntimeConfig {
tokenizer_backend: Some(backend),
..Default::default()
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains(&format!("\"tokenizer_backend\":\"{}\"", backend.as_str())));
let parsed: ModelRuntimeConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.tokenizer_backend, Some(backend));
}
}

#[test]
fn tokenizer_backend_string_values_are_strict() {
for backend in [
TokenizerBackend::Default,
TokenizerBackend::Fastokens,
TokenizerBackend::Basetenkenizer,
] {
assert_eq!(backend.as_str().parse(), Ok(backend));
}

let error = "baseten".parse::<TokenizerBackend>().unwrap_err();
assert!(error.contains("basetenkenizer"));
}

#[test]
Expand Down
Loading
Loading