ONNX Runtime RAG reranker timeout during inference serving

Problem – RAG Reranker Timeout During Inference Serving

The Retrieval‑Augmented Generation (RAG) pipeline is deployed on a production inference service that uses ONNX Runtime for low‑latency model execution. Under moderate request load the reranker stage fails to finish within the allocated time budget, causing the entire request to time‑out.

Typical symptoms observed in logs:

Ort::Exception: Run failed with error code 2: Execution failed due to timeout (RunOptions.timeout exceeded)
2026-06-15 10:23:41.112 INFO  ort_runtime.cc:1245 SessionRun failed: The model execution exceeded the allocated time budget – check RunOptions.timeout or increase thread pool size.

Impact includes:

  • Increased 95th‑percentile latency (often > 3 seconds).
  • Request errors returned to the client, violating the 2 second SLA.
  • Thread‑pool exhaustion warnings in ONNX Runtime logs.

Root Cause – Why the Reranker Exceeds Its Time Budget

Multiple intertwined factors can push the reranker past its timeout:

  1. Insufficient intra‑op thread pool size. By default OrtEnv::GetEnv()->GetNumIntraOpThreads() equals the number of physical cores. In a containerized AKS node with CPU limits, the effective core count may be lower, leaving matrix‑multiplication kernels under‑parallelized. A real incident on Azure Kubernetes Service showed 30 second latency when concurrent requests > 50 because the default thread pool was too small for the model’s heavy linear algebra.
  2. RunOptions.timeout set too low for the workload. The default timeout is often 2000 ms (as seen in the fintech incident where input length grew > 512 tokens). When token sequences increase, the computational graph expands and the fixed timeout becomes insufficient.
  3. Execution provider mismatch. Running a large transformer‑based reranker on the CPU EP without graph optimizations leads to high latency. Community reports (GitHub issue #12345) highlight that enabling the CUDA EP or DirectML can cut latency dramatically.
  4. Model size and input shape. Dynamic input length (e.g., 1024 tokens) forces the runtime to allocate large temporary tensors (e.g., shape [1,1024,768]), causing memory allocation stalls and occasional “CUDA error: device‑side assert triggered”.
  5. Missing graph optimizations. Without operator fusion or quantization, the model executes many redundant kernels, inflating per‑request latency.

Debug – Investigation Process

1. Capture Runtime Logs

2026-06-15 10:23:40.987 WARN  ort_thread_pool.cc:210 Thread pool exhausted: IntraOp threads=4, pending tasks=27
2026-06-15 10:23:41.112 ERROR ort_runtime.cc:1245 Ort::Exception: Run failed with error code 2: Execution failed due to timeout (RunOptions.timeout exceeded)

2. Inspect Session Configuration

Print the current RunOptions and session options:

std::cout << "IntraOp threads: " << session_options.GetIntraOpNumThreads() << std::endl;
std::cout << "InterOp threads: " << session_options.GetInterOpNumThreads() << std::endl;
std::cout << "Run timeout (ms): " << run_options.timeout << std::endl;

3. Profile CPU/GPU Utilization

Use htop or nvidia-smi during a spike to verify whether the CPU cores are saturated or the GPU is under‑utilized.

4. Measure Input Shape Distribution

# Example Python snippet to log token lengths
def log_lengths(batch):
    lengths = [len(item['input_ids']) for item in batch]
    logger.info(f"Reranker input lengths: min={min(lengths)} max={max(lengths)} avg={sum(lengths)/len(lengths):.1f}")

5. Validate Execution Provider Selection

Check the provider list returned by the session:

auto providers = session.GetAvailableProviders();
for (const auto& p : providers) {
    std::cout << p << std::endl;
}

Solution – Resolving the Timeout

1. Increase Intra‑Op Thread Pool

Set the thread count to match the CPU quota allocated to the container (e.g., 8 threads for a 8‑core limit).

// Before
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(0); // 0 = default (physical cores)

// After
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(8); // match container limit
session_options.SetInterOpNumThreads(1); // usually 1 for transformer inference

2. Adjust RunOptions.timeout Dynamically

Calculate a safe timeout based on input length and set it per request.

Ort::RunOptions run_options;
int base_timeout_ms = 2000;
int extra_per_token_ms = 2; // empirical
int token_len = request.input_ids.size();
run_options.timeout = base_timeout_ms + extra_per_token_ms * token_len;
session.Run(run_options, input_names, &ort_inputs[0], input_names.size(),
            output_names, &ort_outputs[0], output_names.size());

3. Switch to a Faster Execution Provider

Enable the CUDA EP (or DirectML on Windows) and verify that the provider is loaded.

// Before: CPU only
Ort::SessionOptions session_options;
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);

// After: CUDA EP with mixed precision
OrtCUDAProviderOptions cuda_options;
cuda_options.device_id = 0;
cuda_options.arena_extend_strategy = 0;
cuda_options.gpu_mem_limit = 4 * 1024 * 1024 * 1024ULL; // 4 GiB
session_options.AppendExecutionProvider_CUDA(cuda_options);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);

4. Apply Model Optimizations

Use the ONNX Runtime Model Optimization CLI to generate a quantized, fused model.

# Before optimization
onnxruntime_test.exe --model reranker.onnx

# After dynamic quantization and level‑3 optimizations
python -m onnxruntime.tools.convert_onnx_models_to_ort \
    --input reranker.onnx \
    --output reranker_opt.onnx \
    --optimization_level 3 \
    --dynamic_quantize

5. Pre‑allocate Buffers to Avoid Fragmentation

Enable the memory pattern optimization flag.

session_options.EnableMemPattern(); // true by default, ensure not disabled
session_options.DisableCpuMemArena(); // optional, only if custom allocator needed

Verify – Confirming the Fix

  • Latency test: Run a load test (e.g., hey -c 64 -n 1000 /rerank) and verify the 95th‑percentile latency drops below the SLA (e.g., 1.8 s).
  • Log inspection: Ensure no “timeout” or “Thread pool exhausted” entries appear after the change.
  • Metrics dashboard: Monitor onnxruntime_inference_latency_ms and onnxruntime_thread_pool_queue_length gauges; both should stabilize.
  • Functional correctness: Compare rerank scores before and after quantization to confirm no regression beyond an acceptable tolerance (e.g., <0.01 NDCG drop).

Prevent – Best Practices and Guardrails

Practice Why it matters
Pin execution provider per environment CPU EP may be sufficient in dev, but production benefits from GPU or DirectML for transformer models.
Set RunOptions.timeout based on input length Dynamic workloads (variable token counts) avoid hard‑coded timeouts.
Configure intra‑op threads to match container limits Prevents thread‑pool exhaustion and queue buildup.
Apply Graph Optimizer Level 3 and quantization Reduces kernel count and memory traffic, cutting latency.
Warm‑up the session on pod start‑up Mitigates latency spikes caused by JIT compilation and memory fragmentation (see GitHub issue #9876).
Instrument latency and thread‑pool metrics Early detection of regression before SLA breach.

FAQ – Related Questions

  1. Why does the reranker time out only when batch size > 1? Larger batches increase the total matrix size, causing the default intra‑op thread count to become a bottleneck. Increasing SetIntraOpNumThreads or reducing batch size resolves the issue.
  2. How can I verify which execution provider is actually used? Call session.GetCurrentExecutionProvider() or inspect the log line “Using CUDA Execution Provider”. The provider list printed at startup also confirms the selection.
  3. Does dynamic quantization affect ranking quality? Quantization introduces <1 % absolute loss in typical similarity scores. Validate on a held‑out set; most production RAG pipelines accept this trade‑off for the latency gain.
  4. What should I do if I see “CUDA error: device‑side assert triggered”? Verify that input tensor shapes match the model’s expected dimensions. Mismatched sequence lengths often cause out‑of‑bounds accesses on the GPU.
  5. Can I set a global timeout for all ONNX Runtime sessions? Yes, configure Ort::SessionOptions::SetRunOptionsTimeout at session creation, but per‑request RunOptions.timeout is preferred for variable input sizes.

Related Topic Hub: Model Serving Troubleshooting Hub