vLLM RAG hybrid search weight imbalance on edge node after config change

Problem

After a recent configuration change on an edge‑computing node running vLLM, the Retrieval‑Augmented Generation (RAG) hybrid search started to favor one modality almost exclusively. In practice:

  • Keyword‑based retrieval (bm25) returns no results even when the query contains exact terms.
  • Dense vector retrieval dominates the relevance score, causing unrelated passages to be selected.
  • Latency spikes because the dense encoder is invoked for every request despite the edge node’s limited compute budget.

The issue is reproducible across multiple deployments after the same configuration file was applied.

Root Cause

The imbalance originates from how vLLM parses the hybrid_search_weights parameter on startup. The relevant code path expects a JSON‑encoded list of two floating‑point numbers representing the weight for the dense vector retriever and the keyword retriever, respectively:

# Expected format (Python dict)
{
    "hybrid_search_weights": [0.5, 0.5]
}

During the last deployment the configuration was supplied via an environment variable:

export VLLM_HYBRID_SEARCH_WEIGHTS="0.8,0.2"

Because the value is not valid JSON, the parser falls back to its default:

  • Dense weight = 1.0
  • Keyword weight = 0.0

Consequently, the keyword branch is never executed, leading to the observed dominance of dense retrieval. The bug is silent – vLLM logs only a generic “using hybrid search weights” message without showing the parsed values.

Debug

Below is a step‑by‑step investigation that isolates the problem.

1. Inspect server startup logs

[2026-10-08 14:12:03,210] INFO  vllm.engine  - Starting vLLM server on 0.0.0.0:8000
[2026-10-08 14:12:03,212] INFO  vllm.engine  - Hybrid search enabled
[2026-10-08 14:12:03,213] INFO  vllm.engine  - Using hybrid search weights: [1.0, 0.0]

The log line shows the parsed weights. The default [1.0, 0.0] is a red flag.

2. Dump the effective configuration via the health endpoint

curl -s http://localhost:8000/v1/config | jq .hybrid_search_weights
# Output
[
  1.0,
  0.0
]

3. Verify the environment variable content

echo $VLLM_HYBRID_SEARCH_WEIGHTS
# Output
0.8,0.2

The missing brackets and quotes confirm the malformed JSON.

4. Run a manual retrieval to see score composition

python - <<'PY'
import requests, json
payload = {
    "model": "my-llm",
    "prompt": "What is the capital of France?",
    "retrieval": {
        "type": "hybrid",
        "query": "capital of France"
    }
}
resp = requests.post("http://localhost:8000/v1/completions", json=payload)
print(json.dumps(resp.json(), indent=2))
PY

Sample fragment of the response:

{
  "retrieval_results": [
    {
      "doc_id": "doc_42",
      "dense_score": 0.93,
      "keyword_score": 0.00,
      "combined_score": 0.93
    },
    ...
  ]
}

The keyword_score is always zero, confirming that the keyword retriever is never invoked.

Solution

Fix the configuration so that vLLM receives a valid JSON list. Two approaches are common on edge nodes:

Option A – Correct the environment variable

# Old (invalid)
export VLLM_HYBRID_SEARCH_WEIGHTS="0.8,0.2"

# New (valid JSON)
export VLLM_HYBRID_SEARCH_WEIGHTS='[0.6, 0.4]'

Restart the service after updating the variable.

Option B – Use a dedicated YAML/JSON config file

Create /etc/vllm/config.yaml with explicit weights:

# /etc/vllm/config.yaml
model: my-llm
hybrid_search_weights: [0.5, 0.5]

Start the server with the config flag:

vllm serve --config /etc/vllm/config.yaml

Before / After comparison

Aspect Before After
Env var value "0.8,0.2" '[0.6, 0.4]'
Parsed weights (log) [1.0, 0.0] [0.6, 0.4]
Keyword score (sample) 0.00 0.31
Combined latency (ms) 210 132

Why it works: vLLM now receives a proper JSON list, the parser assigns 0.6 to the dense retriever and 0.4 to the keyword retriever, and both branches are executed during hybrid search.

Verify

After applying the fix, perform the following checks:

  1. Log confirmation
    [2026-10-08 15:03:11,045] INFO  vllm.engine  - Using hybrid search weights: [0.6, 0.4]
    
  2. Endpoint dump
    curl -s http://localhost:8000/v1/config | jq .hybrid_search_weights
    # Output
    [
      0.6,
      0.4
    ]
    
  3. Score distribution – run the same Python snippet as in the debug phase and verify non‑zero keyword_score values.
  4. Latency benchmark – measure 100 requests with ab or hey and ensure average latency stays within the edge node’s SLA (e.g., < 150 ms).
  5. Functional relevance – manually inspect the top‑3 retrieved passages for a set of known queries; both dense and keyword matches should appear.

Prevent

To avoid recurrence on constrained edge deployments:

  • Configuration validation: add a start‑up hook that parses VLLM_HYBRID_SEARCH_WEIGHTS with json.loads and aborts if the result is not a list of two floats summing to ≤ 1.0.
  • Explicit defaults: ship a minimal config.yaml with sane weights (e.g., [0.5, 0.5]) and document that environment overrides must be valid JSON.
  • Monitoring: expose a Prometheus metric vllm_hybrid_weights{type="dense"} and {type="keyword"}. Alert if either metric is 0 for more than 5 minutes.
  • Edge‑node resource guardrails: tie the dense weight to available GPU memory. For example, a script can compute dense_weight = min(1.0, gpu_mem_gb / 8) and set the env var accordingly before launching vLLM.
  • Automated tests: in CI, spin up a lightweight vLLM instance with a mock model and assert that the health endpoint returns the expected weight list.

Related Topic Hub: Model Serving Troubleshooting Hub

FAQ

  1. Why does the keyword retriever work locally but not on the edge node?
    The local environment used a YAML config with proper JSON syntax, while the edge node relied on an environment variable that lacked brackets. The parser fell back to defaults, disabling the keyword branch.
  2. Can I set different weights per model?
    Yes. vLLM supports a per‑model configuration block. Example:

    models:
      - name: "my-llm"
        hybrid_search_weights: [0.7, 0.3]
      - name: "small-llm"
        hybrid_search_weights: [0.4, 0.6]
    
  3. What is the maximum total weight?
    vLLM does not enforce a sum‑to‑one constraint; the weights are used as multiplicative factors. However, keeping the sum ≤ 1.0 simplifies reasoning about score scaling and avoids unintentionally amplifying the combined score.
  4. How can I debug a similar issue with other hybrid modalities (e.g., BM25 + sparse embeddings)?
    Inspect the same log line (Using hybrid search weights) and the health endpoint. Ensure each modality’s weight is supplied as a JSON list in the same order defined by the retrieval.modality_order config.
  5. Is there a way to make the system automatically rebalance weights based on latency?
    vLLM does not include an auto‑rebalancer out of the box, but you can implement a sidecar that monitors vllm_retrieval_latency_seconds{modality="dense"} and adjusts VLLM_HYBRID_SEARCH_WEIGHTS via a rolling restart when the dense latency exceeds a threshold.