Implementation of the paper, Cluster, Route, Escalate: Cascaded Framework for Cost-Aware LLM Serving.
Production deployment of large language models forces a trade-off between accuracy and cost. This framework routes each query to the most cost-effective model in a pool, then escalates low-quality outputs to a stronger model:
-
Stage 1 (clustering-based routing). Queries are embedded and
clustered offline; each cluster is assigned to the model minimizing
Error(m, c) + lambda * Cost_norm(m), wherelambda($\lambda$ , the cost weight) is tuned once tolambda*($\lambda^*$ ) to satisfy a Time Per Output Token (TPOT) budget. -
Stage 2 (quality-estimation cascade). A lightweight classifier
inspects each efficient-model output and escalates low-quality answers to
a stronger model. With more than two models, escalation follows an ordered
ladder (weakest to strongest, following the
$\lambda$ table): each model has its own classifier and the router rolls up to the next stronger model until an output is accepted or the top is reached.
Both stages train only on task-correctness labels obtainable from standard benchmark evaluation; no extra annotation is required.
The full workflow is driven by the cre CLI, one stage per step:
cre cluster → cre evaluate → cre fit → cre qe-train → cre serve
Serving (cre serve) switches between the live vLLM backends through LiteLLM.
| Stage | What it does | Input | Output | Key options |
|---|---|---|---|---|
cre cluster |
Embeds training queries and fits k-means centroids (k chosen by Silhouette) | JSONL of training queries |
centroids.npy and router.json; train_assignments.jsonl
|
--k, --embedding-model
|
cre evaluate |
Runs each model per cluster through vLLM's benchmark, scores answers, averages per-cluster error and TPOT. This is the slowest stage; cost scales with model size, output length, and --runs, and it runs once per model. |
dataset JSONL, a running vLLM server, fitted centroids | per-model entry in the stats JSON; raw runs under results/
|
--runs, --port, --concurrency
|
cre fit |
Pareto-prunes the pool, sweeps |
stats JSON, budget B | routing table and router.json
|
--budget (required), --output
|
cre qe-train |
Fine-tunes ModernBERT-base as the accept/escalate QE classifier | HF dataset of model outputs with correctness labels | QE classifier checkpoint |
--learning-rate, --max-length, --attn-implementation
|
cre serve |
Runs the live router: sends each incoming query to its cluster's assigned model, and escalates weak answers to a stronger model | serving config, running backends | live HTTP router on port 4000 |
--config, --port
|
Full flags for any stage: cre <stage> --help.
pip install "cre-router[full]"This is the whole pipeline on one machine: clustering and routing, the cascade router, QE classifier training/evaluation, and vLLM for measurement and for hosting backend models. If you want a narrower install, pick from the extras below.
| Extra | Adds | For |
|---|---|---|
| (core) | numpy, scikit-learn, sentence-transformers, pyyaml, anyio | cre cluster, cre fit — always installed |
serve |
+ litellm, fastapi, uvicorn, httpx, torch, transformers | cre serve — the Stage 1 + Stage 2 cascade router (loads ModernBERT in-process) |
qe |
+ torch, transformers, datasets, accelerate | cre qe-train, cre qe-eval — train and evaluate the QE classifier |
eval |
+ vllm | cre evaluate — per-cluster measurement; also provides vllm serve for backends |
full |
qe + serve + eval | the whole pipeline on one machine |
The vLLM backends the router talks to are separate processes started with
vllm serve <model> (cf.
Quickstart: serve CRE-Router); a
serve-only host still needs vLLM installed wherever those backends run.
Efficient QE training also needs FlashAttention
(flash-attn), installed as a second step once torch is already present
(e.g. after pip install "cre-router[qe]"):
pip install flash-attn --no-build-isolation(the paper used flash-attn==2.8.3). The flag matters: without it, pip's
isolated build environment can't see your installed torch/CUDA, so the build
targets the wrong version. Without flash-attn at all, pass
cre qe-train --attn-implementation sdpa to fall back.
A standalone, zero-setup demo of the routing math itself, no serving and no
GPU involved. Running it reproduces the paper's routing table and $\lambda^$
selection directly from the checked-in stats, so you can see how Stage 1
decides which model handles which cluster before setting up any backends.
It is not a prerequisite for the end-to-end GPU-based serving,
which fits its own routing table as one of its steps (cf.
Quickstart: serve CRE-Router).
The per-cluster
stats measured in the paper are checked in under
configs/, so the routing table and budgeted $\lambda^$ reproduce
without any GPU:
cre fit --stats configs/aime_stats.json --budget 20This prints the Pareto analysis, the cre cluster and cre evaluate on your
own dataset and models, the same commands used for the paper (see the
Pipeline table above). If you want to reproduce the paper's exact
numbers with its datasets and models, see REPRODUCE.md instead.
This walks through standing up the live router end to end, using the paper's AIME pool as a concrete, runnable example. Swap in your own models, dataset, and config the same way once you see the shape of it.
-
Start one vLLM server per pool model.
--max-model-lenmust cover input + the task's generation length (40,960 for AIME), or long outputs truncate:vllm serve WeiboAI/VibeThinker-1.5B --port 8001 --max-model-len 42000 vllm serve Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 --port 8002 --max-model-len 42000
-
Fit centroids and write the routing table into an artifacts directory. Reusing the checked-in stats skips the GPU measurement step:
python data/download.py --dataset ymoslem/AIME-clustered --split train --output data/aime_train.jsonl cre cluster --input data/aime_train.jsonl --embeddings-field embeddings --output artifacts/aime cre fit --stats configs/aime_stats.json --budget 20 --output artifacts/aime
-
Write a serving config. It carries only deployment wiring — the model pool (each model's endpoint) and the QE classifier checkpoints. The routing table,
$\lambda^*$ , and the escalation ladder are read from the artifacts and the arithmetic, never set by hand. Pointexample_config_aime24.yamlat your backends and serve (a TeleQnA config,example_config_teleqna.yaml, is also provided):cre serve --config example_config_aime24.yaml
-
Send a standard chat-completions request:
curl http://localhost:4000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"messages": [{"role": "user", "content": "..."}]}'
Routing decisions are returned as
x-cre-cluster,x-cre-stage1-model,x-cre-final-model,x-cre-escalated, andx-cre-pathresponse headers. Streaming is not supported: Stage 2 must inspect the complete response before deciding whether to escalate it. -
Monitor the deployment.
GET /statsreturns live tallies (total requests, escalation rate, and counts per cluster, per final model, and per escalation path). Settingdecision_log: <path>in the config also appends one JSON record per request to that file. Both are designed to add no latency to requests:/statsis in-memory counters, and the decision log is written by a background task fed through a non-blocking queue.
Models. Any model you can serve behind a chat-completions HTTP endpoint (the paper uses vLLM). The pool is arbitrary and can mix sizes and families; routing, Pareto pruning, and the escalation ladder adapt to whatever pool you measure. The paper's pools are Qwen 3 / Qwen 3.5, Gemma 4, and VibeThinker (see REPRODUCE.md).
Datasets. cre evaluate ships two tasks, aime (numeric answers) and
teleqna (multiple choice), each defining an answer parser and sampling
preset. A new dataset with a different answer format needs one new Task
entry (a parser + sampling) in evaluate.py;
clustering, routing, and the QE cascade are domain-agnostic and need no
changes.
Inputs are JSONL, one object per line. Cluster ids are strings ("0", "1", ...) throughout.
- Training queries (
cre cluster):prompt(text to embed). Any other fields are ignored. - Evaluation dataset (
cre evaluate):prompt(sent to the model),answer(ground truth), and optionallycluster. Withoutcluster, pass--artifactsand each row is assigned to its nearest fitted centroid. - QE dataset (
cre qe-train,cre qe-eval):question,full_output(the model's completion),num_tokens(output length), anddecision_label(1 = accept, 0/2 = escalate). Matches the releasedymoslem/*-routerdatasets. - Model stats (
cre fit): JSON withcluster_sizesand per-modelerrorsandcluster_tpot_ms; seeconfigs/aime_stats.json. - Serving config (
cre serve): YAML; seeexample_config_aime24.yaml.
An artifacts directory (written by cre cluster / cre fit) holds
centroids.npy, router.json (embedding model, routing table, train_assignments.jsonl.
cre-router/
├── src/cre_router/
│ ├── clustering.py Stage 1: embed queries, fit k-means centroids
│ ├── routing.py Stage 1: cost-aware routing, Pareto, lambda*
│ ├── evaluate.py Stage 1: measure per-cluster accuracy and TPOT via vLLM
│ ├── artifacts.py load/save centroids + routing table
│ ├── cli.py the `cre` entry point
│ ├── qe/ Stage 2: QE classifier
│ │ ├── train.py fine-tune the accept/escalate classifier
│ │ ├── classifier.py inference wrapper used by the router
│ │ └── evaluate.py standalone QE metrics (`cre qe-eval`)
│ └── server/ live cascade router
│ ├── cascade_router.py Stage 1 routing + Stage 2 escalation ladder
│ ├── app.py HTTP endpoint, /stats, decision log
│ ├── example_config_aime24.yaml serving config (AIME)
│ └── example_config_teleqna.yaml serving config (TeleQnA)
├── configs/ checked-in per-model stats for `cre fit`
├── data/download.py fetch released datasets from the Hugging Face Hub
├── img/system.svg architecture figure
├── tests/ unit tests (routing math vs paper, cascade, ...)
├── requirements-paper.txt frozen environment behind the paper's numbers
├── REPRODUCE.md reproducing the paper
├── DEVELOP.md local development and running the tests
└── README.md
@article{moslem2026clusterrouteescalate,
title={Cluster, Route, Escalate: Cascaded Framework for Cost-Aware LLM Serving},
author={Yasmin Moslem and Magdalena Kacmajor and Vasudevan Nedumpozhimana and Ammar Abbas and Solmaz Panahi and David Lynch and Zhuangzhuang Nie and Alexandros Agapitos and Aleksandar Milenovic and Hongmeng Song and Yucheng Shi and Yue Pan and Patricia Buffini and John D. Kelleher},
year={2026},
eprint={2606.27457},
archivePrefix={arXiv},
primaryClass={cs.PF},
url={https://arxiv.org/abs/2606.27457},
}For local setup and running the test suite, see DEVELOP.md.
Apache-2.0. See LICENSE.