Repository navigation
[TRTLLM-13035][feat] Native /v1/embeddings dynamic batching for encoder-only models #15424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
xwang233
merged 35 commits into
NVIDIA:main
from
xwang233:feat/trtllm-13035-encoder-embeddings
Jul 10, 2026
Merged
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
ef92146
[TRTLLM-13035][feat] Add /v1/embeddings dynamic batching for encoder-…
xwang233 3846168
[TRTLLM-13035][feat] Drop --tp_size from embeddings command (single-G…
xwang233 d3b53cd
[TRTLLM-13035][feat] Trim unused gpus_per_node/free_gpu_memory_fracti…
xwang233 82e8b83
[TRTLLM-13035][doc] Add GPU scale-out guidance to embeddings doc
xwang233 57c7d9c
[TRTLLM-13035][test] Add PRM-7B reward-model coverage to /v1/embeddin…
xwang233 985bc58
[TRTLLM-13035][test] Register /v1/embeddings serve test in l0_l40s
xwang233 3720019
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 b9587c5
[TRTLLM-13035][fix] Address review: engine-sourced limits, Pydantic, …
xwang233 d9b9e9f
[TRTLLM-13035][feat] Add Qwen3ForTextEmbedding (last-token pool + L2 …
xwang233 9b23e75
[TRTLLM-13035][feat] Route trtllm-serve embeddings to Qwen3ForTextEmb…
xwang233 22c5313
[TRTLLM-13035][test] Add Qwen3-Embedding encode() accuracy test vs HF
xwang233 32d80fe
[TRTLLM-13035][test] Add Qwen3-Embedding serve-level /v1/embeddings test
xwang233 0d907b8
[TRTLLM-13035][test] Register Qwen3-Embedding accuracy test in l0_l40s
xwang233 691af3d
[TRTLLM-13035][doc] Document Qwen3-Embedding serving + instruction pr…
xwang233 5413706
[TRTLLM-13035][fix] Qwen3ForTextEmbedding: prepend model. prefix to S…
xwang233 a9596aa
[TRTLLM-13035][fix] Address Qwen3-Embedding review feedback
xwang233 35dba3e
[TRTLLM-13035][test] Cover both Qwen3-Embedding 0.6B and 8B
xwang233 3351556
[TRTLLM-13035][doc] Drop instruction-prefix note from embeddings guide
xwang233 306f6dc
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 47b244f
[TRTLLM-13035][fix] Address embeddings serve review feedback
xwang233 5945257
[TRTLLM-13035][test] Fix dimensions validation status code expectation
xwang233 8d509bd
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 e3ebd25
[TRTLLM-13035][test] Register encode batcher and embedding routing un…
xwang233 ef263a9
[TRTLLM-13035][test] Add encoder embeddings tests to QA function lists
xwang233 7fa2e61
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 045d2e3
[TRTLLM-13035][chore] Address review: single backticks, docstring-to-…
xwang233 44707c3
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 0cd76a1
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 585d833
[TRTLLM-13035][fix] Keep encode batcher worker alive and report its h…
xwang233 2839168
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 3bde83f
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 61dc507
Merge branch 'main' into feat/trtllm-13035-encoder-embeddings
xwang233 7640d22
Merge remote-tracking branch 'origin/main' into feat/trtllm-13035-enc…
xwang233 ddfc572
Merge remote-tracking branch 'fork/feat/trtllm-13035-encoder-embeddin…
xwang233 56087d6
Merge branch 'main' into feat/trtllm-13035-encoder-embeddings
xwang233 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| # Embeddings (Encoder-Only Models) | ||
|
|
||
| `trtllm-serve` can serve **encoder-only models** (BERT-style classifiers, reward | ||
| models, text-embedding models) through an OpenAI-compatible **`POST /v1/embeddings`** | ||
| endpoint with **native dynamic batching** — coalescing many independent concurrent | ||
| requests into a single forward pass for high throughput, the way the NVIDIA Triton | ||
| Inference Server dynamic batcher does. | ||
|
|
||
| This replaces the need to run a separate Triton Inference Server in front of an | ||
| encoder model: point your existing OpenAI-style embeddings client at `trtllm-serve` | ||
| and it works unchanged. | ||
|
|
||
| ## Quick start | ||
|
|
||
| Launch an embeddings server with the `embeddings` subcommand: | ||
|
|
||
| ```bash | ||
| trtllm-serve embeddings <hf_model_or_path> \ | ||
| --max_batch_size 32 \ | ||
| --max_queue_delay 0.005 \ | ||
| --max_queue_size 2048 \ | ||
| --host 0.0.0.0 --port 8000 | ||
| ``` | ||
|
|
||
| Send a request with any OpenAI-compatible client or `curl`: | ||
|
|
||
| ```bash | ||
| curl http://localhost:8000/v1/embeddings \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"model": "<model>", "input": ["hello world", "foo bar"]}' | ||
| ``` | ||
|
|
||
| ```python | ||
| from openai import OpenAI | ||
|
|
||
| client = OpenAI(base_url="http://localhost:8000/v1", api_key="tensorrt_llm") | ||
| resp = client.embeddings.create(model="<model>", input=["hello world", "foo bar"]) | ||
| for item in resp.data: | ||
| print(item.index, len(item.embedding)) | ||
| ``` | ||
|
|
||
| The response is the standard OpenAI embeddings shape: | ||
|
|
||
| ```json | ||
| { | ||
| "object": "list", | ||
| "data": [ | ||
| {"object": "embedding", "index": 0, "embedding": [ ... ]}, | ||
| {"object": "embedding", "index": 1, "embedding": [ ... ]} | ||
| ], | ||
| "model": "<model>", | ||
| "usage": {"prompt_tokens": 8, "total_tokens": 8} | ||
| } | ||
| ``` | ||
|
|
||
| ## Request fields | ||
|
|
||
| The endpoint accepts the standard OpenAI `/v1/embeddings` fields: | ||
|
|
||
| | Field | Type | Notes | | ||
| |---|---|---| | ||
| | `model` | str | Model name. | | ||
| | `input` | str \| list[str] \| list[int] \| list[list[int]] | Text(s) or pre-tokenized token-id list(s). | | ||
| | `encoding_format` | `"float"` (default) \| `"base64"` | `base64` packs little-endian float32 values. | | ||
| | `dimensions` | int (optional) | Matryoshka output size. Only supported by Matryoshka-trained text-embedding models; rejected with `400` otherwise. None of the served models are Matryoshka-trained (BERT classifiers / reward models emit label/score tensors; Qwen3-Embedding emits a fixed-width pooled vector), so this is currently always rejected. | | ||
| | `user` | str (optional) | Ignored; accepted for compatibility. | | ||
| | `add_special_tokens` | bool (default `true`) | TRT-LLM extension. Encoder models such as BERT generally need their special tokens (e.g. `[CLS]`/`[SEP]`) added during tokenization. | | ||
|
|
||
| There are **no required TRT-LLM-specific request fields** — existing OpenAI-compatible | ||
| embeddings clients work by pointing at the `trtllm-serve` URL. | ||
|
|
||
| ## Dynamic batching | ||
|
|
||
| A lightweight in-server batcher coalesces concurrent requests in front of the | ||
| encoder forward pass. It exposes three knobs that mirror the Triton dynamic batcher: | ||
|
|
||
| | `trtllm-serve embeddings` flag | Behavior | Triton equivalent | | ||
| |---|---|---| | ||
| | `--max_batch_size` | Upper bound on the number of requests fused into one forward pass. A batch reaching this size is dispatched immediately. | maximum / `preferred_batch_size` | | ||
| | `--max_queue_delay` (seconds) | Hold window: how long an incoming request waits for others to join its batch before dispatch. | `max_queue_delay_microseconds` | | ||
| | `--max_queue_size` | Maximum number of in-flight queued requests. Further requests are rejected with HTTP 429 (backpressure). | `default_queue_policy.max_queue_size` | | ||
|
|
||
| A batch is dispatched as soon as **any** of these fires: it reaches `--max_batch_size`, | ||
| adding the next request would exceed the engine's `--max_num_tokens` budget, or the | ||
| `--max_queue_delay` hold window elapses. | ||
|
|
||
| ### Migrating from the Triton Inference Server dynamic batcher | ||
|
|
||
| If you currently serve an encoder model with the Triton `inflight_batcher_llm` backend | ||
| and a `config.pbtxt` `dynamic_batching { ... }` block, map the settings directly: | ||
|
|
||
| | Triton `config.pbtxt` | `trtllm-serve embeddings` | | ||
| |---|---| | ||
| | `dynamic_batching.preferred_batch_size` / model max batch | `--max_batch_size` | | ||
| | `dynamic_batching.max_queue_delay_microseconds` | `--max_queue_delay` (in **seconds**, e.g. `100 µs` → `0.0001`) | | ||
| | `dynamic_batching.default_queue_policy.max_queue_size` | `--max_queue_size` | | ||
|
|
||
| Adopt the same values you tuned in Triton as a starting point, then adjust for your | ||
| latency/throughput budget. | ||
|
|
||
| ## Error handling | ||
|
|
||
| | Condition | HTTP status | | ||
| |---|---| | ||
| | Input longer than `--max_seq_len` | 400 | | ||
| | Request queue full (`--max_queue_size` reached) | 429 | | ||
| | Invalid request body | 400 | | ||
|
|
||
| Embedding responses are unary (non-streaming). | ||
|
|
||
| ## Output semantics and scope | ||
|
|
||
| The endpoint is **model-output-agnostic**: it returns whatever per-request vector the | ||
| model emits, serialized into the OpenAI embeddings schema. | ||
|
|
||
| - **Classifier / reward models** (e.g. a BERT sequence classifier): the returned | ||
| vector is the model's class-logit / score vector (`[num_labels]`). | ||
| - **Text-embedding models** — the **Qwen3-Embedding family** (`Qwen3-Embedding-0.6B`, | ||
| `-4B`, `-8B`) is supported. These ship as a `Qwen3ForCausalLM` decoder plus a | ||
| sentence-transformers pooling pipeline; the embeddings server detects this and serves | ||
| the **L2-normalized last-token hidden state** (a `[hidden_size]` sentence-embedding | ||
| vector — 1024 / 2560 / 4096 respectively), with no extra flags. A configurable pooling | ||
| method (CLS / mean) for other sentence-transformers backbones remains a follow-up. | ||
|
|
||
| Notes: | ||
|
|
||
| - The embeddings path uses the synchronous `llm.encode()` fast path | ||
| (`EncoderExecutor`): a single forward pass per batch, **no KV cache, sampler, or | ||
| decode loop**. | ||
| - One encoder model per server instance. Generation and embedding modes are not mixed | ||
| in one server. | ||
| - **Single-GPU per server.** The encode path runs in-process and does not use the | ||
| multi-GPU worker proxy, so the `embeddings` command does not expose tensor/pipeline | ||
| parallelism flags (if a `--config` file sets them, startup fails with a clear error). | ||
| To scale out, see [Scaling out across GPUs](#scaling-out-across-gpus) below. | ||
| - A single in-server worker drives the GPU (no `num_workers` knob): the GPU serializes | ||
| forwards and the underlying executor is not safe for concurrent calls. Increase | ||
| throughput with `--max_batch_size` / `--max_queue_delay`, not more workers. | ||
|
|
||
| ## Scaling out across GPUs | ||
|
|
||
| Embedding / encoder-only models are usually small and fit comfortably on a single GPU. | ||
| The recommended way to use more GPUs is therefore **data parallelism**: run one | ||
| single-GPU `trtllm-serve embeddings` instance per GPU and put a load balancer in front | ||
| of them. There is no cross-GPU communication, so throughput scales close to linearly | ||
| with the number of replicas. | ||
|
|
||
| ```bash | ||
| # One replica per GPU (8x B200 example), each on its own port. | ||
| for i in $(seq 0 7); do | ||
| CUDA_VISIBLE_DEVICES=$i trtllm-serve embeddings <model> --port $((8000 + i)) & | ||
| done | ||
| # Then point any HTTP load balancer (nginx, k8s Service, etc.) at ports 8000-8007. | ||
| ``` | ||
|
|
||
| **Tensor / pipeline parallelism** (sharding a single model across GPUs) is only needed | ||
| for an embedding model too large to fit on one GPU — uncommon for encoder-only models. | ||
| It is **not yet supported** by the `embeddings` command and is planned as a follow-up. | ||
|
|
||
| ## Relationship to `llm.encode()` | ||
|
|
||
| The server reuses the existing Python `llm.encode()` API | ||
| (`LLM(..., encode_only=True)`) under the hood; the only addition is the async | ||
| coalescing layer plus the HTTP surface. The synchronous `llm.encode()` API continues | ||
| to work unchanged for direct Python callers. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.