← Hitesh Jain

Domain-Specific Embedding Inference with Sub-20ms Tail Latency

Summary

We fine-tuned a commerce-specific semantic embedding model on a ModernBERT backbone. These embeddings power product search in low-latency applications like typeahead, voice search, and recommendations. Our goal: serve shopper query embeddings with sub-20 ms P95 latency under representative load conditions, so search platforms can spend their milliseconds on filtering and reranking.

We achieved P95 API latency of ~18 ms and P95 inference latency of ~3 ms on a commodity GPU (A10) running at sustained load (100 QPS for 5 minutes) using NVIDIA Triton Inference Server with a TensorRT runtime.

This post details our test environment, compares our initial Python stack to the new Triton architecture, and explains the engineering trade-offs that unlocked these speeds.

Our Workload

Our typical workload involves commerce platforms sending short shopper queries between 16 and 32 tokens (e.g., "best marathon training shoes for women"). Real-time commerce search applications demand strict end-to-end embedding SLAs of 20–40 ms.

Architecture

Two-service architecture to optimize performance and scalability: a gateway service on one host handling CPU-intensive tasks and a GPU inference server on another host focused on maximizing GPU utilization. This separation of CPU and GPU workloads offers two main advantages:

Fixed-length sequence bucketing is a pre-processing step in the gateway that optimizes inputs for the GPU. Queries of varying lengths are sorted into "buckets" based on preset lengths (e.g., 16, 32, 64). The system then pads each query only up to its bucket's ceiling, rather than the maximum possible length. For example, a 20-token query is padded only to 32. This method is critical for minimizing batch shape variety received by the inference server, which eliminates wasted computation and is essential for maximizing GPU utilization and achieving ultra-low latency.

We co-located the embedding services with the load testing client to eliminate network latency associated with cross-region communication.

We experimented with two inference stacks:

Data Flow Diagram

Embedding inference architecture showing the request flow from client through the embedding gateway to both stacks: a PyTorch inference server and a Triton inference server
Figure 1: Embedding inference architecture showing the complete request flow from client through the embedding gateway to both stacks — Python + PyTorch inference server and Triton inference server.

Below, we detail the key technical decisions made for each implementation.

Stack 1: Fully Customized Python + PyTorch

We used torch.compile (mode reduce-overhead) and implemented several optimizations:

Stack 2: Tuned Triton + TensorRT

Trade-Offs: Python + PyTorch vs Triton + TensorRT

We eventually migrated from Python + PyTorch to Triton + TensorRT, reducing P95 inference latency from 7 ms to 3 ms. The key driver was increasing parallel CUDA streams from 1 to 4 for improved GPU utilization, allowing incoming load batches to be split across four model instances.

MetricPython + PyTorchTriton + TensorRT
P5019.35 ms13.47 ms
P9548.51 ms17.73 ms
P9959.23 ms26.41 ms

Table 1: Python + PyTorch vs Triton + TensorRT end-to-end latencies at 100 QPS, batch size 1.

For those who opt to deploy their own inference servers, this is a comparison of both stacks:

AspectPython + PyTorchTriton + TensorRT
Increase data parallelism from 1 to 4Single GPU worker thread. One batch at a time.4 parallel CUDA streams per GPU. Concurrent batches. TensorRT engine.
Development overheadFamiliar Python / Hugging Face helped getting started quicklyRamp up on Triton + TensorRT C++ tooling took longer

Table 2: Comparison of the Python + PyTorch and Triton + TensorRT architectures.

Load Test: 100 QPS, 300 Seconds, Batch 1

Test Architecture

The load test runs against the production embedding API. The request path is:

Client (EC2, same VPC) → NLB → embedding gateway → inference server

Test Configuration

ParameterValue
ModelModernBERT-based, 768-dimensional
ModeQuery (short search queries, up to 32 tokens)
Target QPS100
Duration300 seconds
Batch size1
Load sourceEC2 instance in same VPC as gateway and inference server

Table 3: Load test configuration parameters.

Results

End-to-end latency:

PercentileLatency
P5013.47 ms
P9015.58 ms
P9517.73 ms
P9926.41 ms
Mean14.21 ms

Table 4: End-to-end latency distribution.

Triton model latencies:

ModelP50P90P95P99
modernbert_tensorrt2.448 ms2.567 ms2.601 ms2.674 ms
mean_pooling0.429 ms0.498 ms0.546 ms0.765 ms
modernbert_ensemble2.884 ms3.062 ms3.128 ms3.259 ms

Table 5: Server-side Triton model latencies.

Note that percentiles don't add linearly, and inter-model overhead (tensor transfer, Python invocation) is not fully attributed to either model — so the ensemble latency is close to, but not exactly, the sum of the component latencies.

So at batch size 1 and short sequences:

The gap between ~3 ms inference and ~18 ms end-to-end comes from tokenization, rate limiting, auth, Redis, and network — not GPU inference.

Key Takeaways

References