Text Generation
Transformers
Safetensors
English
qwen2
router
llm-router
orchestration
multi-agent
model-selection
fugu
conversational
Eval Results (legacy)
text-generation-inference
Instructions to use podstack/Shaktimaan with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use podstack/Shaktimaan with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="podstack/Shaktimaan") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("podstack/Shaktimaan") model = AutoModelForCausalLM.from_pretrained("podstack/Shaktimaan", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use podstack/Shaktimaan with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "podstack/Shaktimaan" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "podstack/Shaktimaan", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/podstack/Shaktimaan
- SGLang
How to use podstack/Shaktimaan with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "podstack/Shaktimaan" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "podstack/Shaktimaan", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "podstack/Shaktimaan" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "podstack/Shaktimaan", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use podstack/Shaktimaan with Docker Model Runner:
docker model run hf.co/podstack/Shaktimaan
Shaktimaan v1.0
Browse files- .gitattributes +1 -0
- README.md +206 -0
- build.py +93 -0
- chat_template.jinja +54 -0
- common.py +157 -0
- config.json +57 -0
- eval_results.json +127 -0
- evaluate.py +129 -0
- gen.py +198 -0
- generation_config.json +14 -0
- model.safetensors +3 -0
- orchestrate.py +72 -0
- tokenizer.json +3 -0
- tokenizer_config.json +30 -0
- train.py +19 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
base_model: Qwen/Qwen2.5-0.5B-Instruct
|
| 3 |
+
license: apache-2.0
|
| 4 |
+
language: [en]
|
| 5 |
+
datasets: [podstack/Shaktimaan-routing]
|
| 6 |
+
library_name: transformers
|
| 7 |
+
pipeline_tag: text-generation
|
| 8 |
+
tags: [router, llm-router, orchestration, multi-agent, model-selection, fugu]
|
| 9 |
+
model-index:
|
| 10 |
+
- name: Shaktimaan v1.0
|
| 11 |
+
results:
|
| 12 |
+
- task: {type: text-classification, name: LLM routing}
|
| 13 |
+
dataset: {type: podstack/Shaktimaan-routing, name: Shaktimaan routing (held-out test, 554 tasks)}
|
| 14 |
+
metrics:
|
| 15 |
+
- {type: accuracy, value: 0.8339, name: end-to-end task accuracy}
|
| 16 |
+
- {type: relative_cost, value: 0.4240, name: cost vs always-strongest agent}
|
| 17 |
+
---
|
| 18 |
+
# Shaktimaan v1.0 (0.5B)
|
| 19 |
+
|
| 20 |
+
**Shaktimaan** is a 0.5B-parameter LLM orchestrator in the spirit of Sakana Fugu. It reads a task and decides which
|
| 21 |
+
agent(s) from a pool of six LLMs should handle it - one agent (`single`), or several that answer independently with
|
| 22 |
+
the last one aggregating (`ensemble`) - trading off cost, capability (image input, context length) and accuracy. It
|
| 23 |
+
does not answer tasks itself. Fine-tuned from [Qwen2.5-0.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct)
|
| 24 |
+
on [podstack/Shaktimaan-routing](https://huggingface.co/datasets/podstack/Shaktimaan-routing).
|
| 25 |
+
|
| 26 |
+
## At a glance
|
| 27 |
+
- **83.4%** end-to-end accuracy on 554 held-out tasks (math, knowledge,
|
| 28 |
+
science, code, long context, charts) at **0.42x** the cost of always using the
|
| 29 |
+
strongest agent.
|
| 30 |
+
- Beats every single agent used alone (best: Always `glm-5.3-flash` at 79.2%) and the
|
| 31 |
+
strongest agent alone (78.5% at 1.00x).
|
| 32 |
+
- Learns capability routing from data: charts -> the only vision agent, very long inputs -> the long-context agents.
|
| 33 |
+
- **Does not beat hand-written capability rules overall**: Capability rules -> `glm-5.3-flash` reaches
|
| 34 |
+
85.7% at 0.13x. See *Results* and *Findings*.
|
| 35 |
+
- 100% of outputs are valid route JSON (100.0% on the test set).
|
| 36 |
+
|
| 37 |
+
## Output format
|
| 38 |
+
```json
|
| 39 |
+
{"domain": "math", "difficulty": "easy|medium|hard", "strategy": "single|ensemble", "agents": ["glm-5.3-flash"]}
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## Agent pool
|
| 43 |
+
| agent | model | context | image input | relative cost / token |
|
| 44 |
+
|---|---|---|---|---|
|
| 45 |
+
| `gemma-3-1b` | `google/gemma-3-1b-it` | 32k | no | 0.02 |
|
| 46 |
+
| `kimi-vl` | `Kimi-VL-A3B-Thinking` | 8k | yes | 0.05 |
|
| 47 |
+
| `nemotron-lightning` | `nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4` | 128k | no | 0.1 |
|
| 48 |
+
| `glm-5.3-flash` | `zai-org/GLM-5.3-Flash` | 32k | no | 0.2 |
|
| 49 |
+
| `qwen3.8-27b` | `Qwen/Qwen3.8-27B` | 32k | no | 0.5 |
|
| 50 |
+
| `nemotron-ultra` | `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4` | 256k | no | 1.0 |
|
| 51 |
+
|
| 52 |
+
Agents are reached through OpenAI-compatible chat-completions endpoints. "Relative cost" is a per-token weight used
|
| 53 |
+
to compute routing cost (roughly relative serving cost), not a vendor price.
|
| 54 |
+
|
| 55 |
+
## Quick start
|
| 56 |
+
```python
|
| 57 |
+
import json
|
| 58 |
+
from huggingface_hub import hf_hub_download
|
| 59 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 60 |
+
import importlib.util, sys
|
| 61 |
+
|
| 62 |
+
repo = "podstack/Shaktimaan"
|
| 63 |
+
spec = importlib.util.spec_from_file_location("common", hf_hub_download(repo, "common.py"))
|
| 64 |
+
common = importlib.util.module_from_spec(spec); spec.loader.exec_module(common)
|
| 65 |
+
|
| 66 |
+
tok = AutoTokenizer.from_pretrained(repo)
|
| 67 |
+
model = AutoModelForCausalLM.from_pretrained(repo)
|
| 68 |
+
task = "Write a Python function that returns the n-th Fibonacci number."
|
| 69 |
+
msgs = [{"role": "system", "content": common.ROUTER_SYSTEM},
|
| 70 |
+
{"role": "user", "content": common.router_view(task, has_image=False)}]
|
| 71 |
+
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", return_dict=True)
|
| 72 |
+
out = model.generate(**enc, max_new_tokens=96, do_sample=False)
|
| 73 |
+
print(json.loads(tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)))
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
Always build the input with `common.router_view(task, has_image)` - the router was trained on that exact format (a
|
| 77 |
+
`[input ~Nk tokens; 1 image attached]` header plus the first 5,000 and last 1,500 characters of the task) with
|
| 78 |
+
`common.ROUTER_SYSTEM` as the system prompt.
|
| 79 |
+
|
| 80 |
+
### End-to-end orchestration
|
| 81 |
+
`orchestrate.py` routes with Shaktimaan, drops agents that cannot take the input, then calls the chosen agent(s) and
|
| 82 |
+
aggregates ensembles:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
export SHAKTIMAAN_ENDPOINTS='{"kimi-vl": "http://host-a:8000/v1", "nemotron-lightning": "http://host-b:8000/v1", ...}'
|
| 86 |
+
python orchestrate.py "If a train travels 120 km in 1.5 hours, what is its average speed?"
|
| 87 |
+
python orchestrate.py --code "Write a function that checks whether a string is a palindrome."
|
| 88 |
+
python orchestrate.py --image chart.png "What was the revenue in Q2 2021?"
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## Training data
|
| 92 |
+
4989 train / 554 test tasks across eight sources. Every capable agent attempted every
|
| 93 |
+
task once and was graded automatically; the label is the cheapest *reliable* agent that solved the task (or an
|
| 94 |
+
ensemble when none did). Full details, solve rates per agent and the label policy are in the
|
| 95 |
+
[dataset card](https://huggingface.co/datasets/podstack/Shaktimaan-routing).
|
| 96 |
+
|
| 97 |
+
| source | category | train | test | grading | license |
|
| 98 |
+
|---|---|---|---|---|---|
|
| 99 |
+
| [GSM8K](https://huggingface.co/datasets/openai/gsm8k) | math word problems | 895 | 105 | numeric match | MIT |
|
| 100 |
+
| [MATH](https://huggingface.co/datasets/EleutherAI/hendrycks_math) | competition math | 894 | 106 | normalised LaTeX / numeric match | MIT |
|
| 101 |
+
| [MMLU-Pro](https://huggingface.co/datasets/TIGER-Lab/MMLU-Pro) | knowledge MCQ (10 options) | 1080 | 120 | option letter | MIT |
|
| 102 |
+
| [ARC-Challenge](https://huggingface.co/datasets/allenai/ai2_arc) | science MCQ | 725 | 75 | option letter | CC-BY-SA-4.0 |
|
| 103 |
+
| [MBPP](https://huggingface.co/datasets/google-research-datasets/mbpp) | Python coding | 633 | 67 | unit tests executed in a network-less sandbox | CC-BY-4.0 |
|
| 104 |
+
| [HumanEval](https://huggingface.co/datasets/openai/openai_humaneval) | Python coding | 146 | 18 | unit tests executed in a network-less sandbox | MIT |
|
| 105 |
+
| [LongBench-v2](https://huggingface.co/datasets/THUDM/LongBench-v2) | long-context MCQ (up to ~260k tokens) | 347 | 32 | option letter | Apache-2.0 |
|
| 106 |
+
| [ChartQA](https://huggingface.co/datasets/HuggingFaceM4/ChartQA) | chart images | 269 | 31 | relaxed match (numbers within 5%) | GPL-3.0 |
|
| 107 |
+
|
| 108 |
+
## Training
|
| 109 |
+
| setting | value |
|
| 110 |
+
|---|---|
|
| 111 |
+
| base model | Qwen2.5-0.5B-Instruct (494M parameters) |
|
| 112 |
+
| method | full fine-tune, supervised; loss on the route JSON only |
|
| 113 |
+
| epochs | 3 |
|
| 114 |
+
| learning rate | 2e-5, cosine schedule, 20 warm-up steps |
|
| 115 |
+
| effective batch size | 32 |
|
| 116 |
+
| max sequence length | 3,072 tokens |
|
| 117 |
+
| precision | bf16 |
|
| 118 |
+
| final eval loss | 0.0484 |
|
| 119 |
+
|
| 120 |
+
## Results
|
| 121 |
+
Held-out test set: 554 tasks the router never saw. A policy's accuracy is whether the agent it picks
|
| 122 |
+
actually solved the task. Shaktimaan's 31 ensemble decisions were **executed live** (all agents
|
| 123 |
+
plus aggregator called and graded); single-agent decisions use the recorded outcomes. Cost is the sum of
|
| 124 |
+
`relative cost x output tokens`, normalised so that always using `nemotron-ultra` = 1.00.
|
| 125 |
+
|
| 126 |
+
| policy | accuracy | relative cost (always-Ultra = 1.00) |
|
| 127 |
+
|---|---|---|
|
| 128 |
+
| **Shaktimaan v1.0** | 83.4% | 0.424 |
|
| 129 |
+
| Capability rules -> `glm-5.3-flash` | 85.7% | 0.133 |
|
| 130 |
+
| Capability rules -> `qwen3.8-27b` | 85.4% | 0.403 |
|
| 131 |
+
| Capability rules -> `nemotron-ultra` | 82.7% | 1.001 |
|
| 132 |
+
| Always `gemma-3-1b` | 25.6% | 0.002 |
|
| 133 |
+
| Always `kimi-vl` | 61.4% | 0.074 |
|
| 134 |
+
| Always `nemotron-lightning` | 75.6% | 0.109 |
|
| 135 |
+
| Always `glm-5.3-flash` | 79.2% | 0.094 |
|
| 136 |
+
| Always `qwen3.8-27b` | 78.9% | 0.364 |
|
| 137 |
+
| Always `nemotron-ultra` | 78.5% | 1.000 |
|
| 138 |
+
| Oracle (hindsight-best label) | 89.7% | 0.490 |
|
| 139 |
+
|
| 140 |
+
**Capability rules** are a hand-written baseline: image -> `kimi-vl`; input too long for the default agent -> the
|
| 141 |
+
cheapest agent with a large enough context window; otherwise the default agent. **Oracle** is the hindsight-best
|
| 142 |
+
training label (an upper bound; its ensembles are scored as the aggregator's solo answer).
|
| 143 |
+
|
| 144 |
+
### Accuracy by source
|
| 145 |
+
| source | n | Shaktimaan | rules -> glm | rules -> qwen | always Ultra | oracle |
|
| 146 |
+
|---|---|---|---|---|---|---|
|
| 147 |
+
| ARC-Challenge | 75 | 98.7% | 98.7% | 98.7% | 98.7% | 100.0% |
|
| 148 |
+
| ChartQA | 31 | 74.2% | 74.2% | 74.2% | 0.0% | 74.2% |
|
| 149 |
+
| GSM8K | 105 | 89.5% | 96.2% | 98.1% | 97.1% | 98.1% |
|
| 150 |
+
| HumanEval | 18 | 88.9% | 94.4% | 94.4% | 94.4% | 100.0% |
|
| 151 |
+
| LongBench-v2 | 32 | 59.4% | 46.9% | 50.0% | 59.4% | 59.4% |
|
| 152 |
+
| MATH | 106 | 75.5% | 74.5% | 75.5% | 65.1% | 82.1% |
|
| 153 |
+
| MBPP | 67 | 94.0% | 98.5% | 92.5% | 97.0% | 100.0% |
|
| 154 |
+
| MMLU-Pro | 120 | 77.5% | 83.3% | 81.7% | 74.2% | 87.5% |
|
| 155 |
+
|
| 156 |
+
### Routes chosen on the test set
|
| 157 |
+
| route chosen by Shaktimaan | test tasks |
|
| 158 |
+
|---|---|
|
| 159 |
+
| `nemotron-lightning` | 218 |
|
| 160 |
+
| `glm-5.3-flash` | 192 |
|
| 161 |
+
| `kimi-vl` | 80 |
|
| 162 |
+
| `glm-5.3-flash,qwen3.8-27b,nemotron-ultra` | 32 |
|
| 163 |
+
| `nemotron-ultra` | 31 |
|
| 164 |
+
| `qwen3.8-27b` | 1 |
|
| 165 |
+
|
| 166 |
+
## Findings
|
| 167 |
+
- **Routing beats any single model.** Shaktimaan is 4.1
|
| 168 |
+
points above the best single agent and 4.9
|
| 169 |
+
points above always using the strongest (and most expensive) one, at
|
| 170 |
+
0.42x its cost.
|
| 171 |
+
- **Capability routing is learned from data.** On long-context tasks Shaktimaan matches the oracle
|
| 172 |
+
(59.4%) and beats the capability rules (46.9%),
|
| 173 |
+
sending inputs to the agent most likely to solve them rather than merely one that fits. On charts it routes to the
|
| 174 |
+
only vision agent, as the rules do.
|
| 175 |
+
- **Simple capability rules are stronger overall** (85.7% at
|
| 176 |
+
0.13x). On ordinary text and code tasks the mid-size agents are close to
|
| 177 |
+
interchangeable, and which one solves a particular task is largely sampling noise that a router cannot predict from
|
| 178 |
+
the prompt. Shaktimaan sometimes picks the cheaper `nemotron-lightning` or `kimi-vl` where the rule's fixed
|
| 179 |
+
`glm-5.3-flash` default is slightly more reliable, and its ensembles (chosen for hard tasks) solved
|
| 180 |
+
11 of the 31 hard tasks they were sent, at a high cost.
|
| 181 |
+
- **Practical recommendation:** for this pool, apply capability rules first and use Shaktimaan where its learned
|
| 182 |
+
judgement helps most - choosing among long-context agents and flagging hard tasks.
|
| 183 |
+
|
| 184 |
+
## Limitations
|
| 185 |
+
- Routes only among the six agents above, with their context windows and image support; a different pool needs new
|
| 186 |
+
data and retraining.
|
| 187 |
+
- Labels come from a single sample per agent and inherit grading noise (e.g. answer extraction from free text).
|
| 188 |
+
- Not trained on agentic / tool-use, multi-turn or open-ended writing tasks; English only.
|
| 189 |
+
- `est_tokens` uses a conservative characters/3 estimate, so borderline inputs may be routed to a larger-context
|
| 190 |
+
agent than strictly needed.
|
| 191 |
+
|
| 192 |
+
## Files
|
| 193 |
+
| file | purpose |
|
| 194 |
+
|---|---|
|
| 195 |
+
| `model.safetensors`, `config.json`, tokenizer files | the router |
|
| 196 |
+
| `common.py` | agent pool, prompts, `router_view`, capability checks, graders |
|
| 197 |
+
| `orchestrate.py` | end-to-end runtime (route -> call agents -> aggregate) |
|
| 198 |
+
| `gen.py` | builds tasks and collects graded agent outcomes |
|
| 199 |
+
| `build.py` | labels outcomes and builds the train / test split |
|
| 200 |
+
| `train.py` | fine-tuning script |
|
| 201 |
+
| `evaluate.py` | held-out evaluation incl. live ensembles and baselines |
|
| 202 |
+
| `eval_results.json` | raw evaluation output |
|
| 203 |
+
|
| 204 |
+
## License
|
| 205 |
+
Model weights: Apache-2.0 (as the base model). Training data: see the
|
| 206 |
+
[dataset card](https://huggingface.co/datasets/podstack/Shaktimaan-routing) (CC-BY-SA-4.0; ChartQA rows GPL-3.0, not redistributed).
|
build.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 2: turn graded runs into routing labels (chat-format SFT data) with a held-out test split.
|
| 2 |
+
|
| 3 |
+
Label policy (cost-aware oracle):
|
| 4 |
+
* cheapest routable agent that solved the task, counting a win only if the agent is reliable on that source
|
| 5 |
+
(solve rate >= 70%). Wins by unreliable agents are mostly lucky single samples a router cannot predict: v1 sent
|
| 6 |
+
gemma-3-1b 68 held-out tasks and only 53% were solved, and kimi-vl solves just 63% of text tasks that >= 4 other
|
| 7 |
+
agents solve (lightning 95%, glm 98%). gemma-3-1b is excluded as a target outright;
|
| 8 |
+
* if no routable agent solved it -> ensemble of the capable agents among glm / qwen / ultra (aggregated by the
|
| 9 |
+
last one) when at least two can take the task, otherwise the strongest capable agent alone.
|
| 10 |
+
The router sees `router_view(...)`: a size / image header plus the start and end of the task.
|
| 11 |
+
"""
|
| 12 |
+
import json, random
|
| 13 |
+
from collections import Counter
|
| 14 |
+
from datasets import Dataset, DatasetDict
|
| 15 |
+
from common import AGENTS, POOL, ROUTER_SYSTEM, STRONGEST, capable, est_tokens, router_view
|
| 16 |
+
|
| 17 |
+
random.seed(0)
|
| 18 |
+
ROUTABLE = [a for a in AGENTS if a != "gemma-3-1b"]
|
| 19 |
+
ENSEMBLE_POOL = ["glm-5.3-flash", "qwen3.8-27b", STRONGEST]
|
| 20 |
+
RELIABLE = 0.70
|
| 21 |
+
SOLVE_RATE = {} # (source, agent) -> solve rate, filled in main()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def cost(res, name):
|
| 25 |
+
return POOL[name]["cost"] * (res[name]["prompt_tokens"] + res[name]["completion_tokens"])
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def can(task, agent):
|
| 29 |
+
"""Capability check; long prompts are stored truncated, so trust the recorded skip instead of re-estimating."""
|
| 30 |
+
return not task["results"][agent].get("skipped") and capable(agent, "" if task.get("prompt_truncated")
|
| 31 |
+
else task["prompt"], task["has_image"])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def label(task):
|
| 35 |
+
res = task["results"]
|
| 36 |
+
n_correct = sum(res[a]["correct"] for a in AGENTS)
|
| 37 |
+
correct = [a for a in ROUTABLE if res[a]["correct"] and SOLVE_RATE[task["source"], a] >= RELIABLE]
|
| 38 |
+
difficulty = "easy" if n_correct >= 4 else "medium" if n_correct >= 2 else "hard"
|
| 39 |
+
if correct:
|
| 40 |
+
return dict(strategy="single", agents=[min(correct, key=lambda a: cost(res, a))], difficulty=difficulty)
|
| 41 |
+
ens = [a for a in ENSEMBLE_POOL if can(task, a)]
|
| 42 |
+
if len(ens) >= 2 and not task["has_image"]:
|
| 43 |
+
return dict(strategy="ensemble", agents=ens, difficulty="hard")
|
| 44 |
+
fallback = [a for a in ROUTABLE if can(task, a)] or [STRONGEST]
|
| 45 |
+
return dict(strategy="single", agents=[fallback[-1]], difficulty="hard")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def to_example(task):
|
| 49 |
+
lab = label(task)
|
| 50 |
+
target = json.dumps({"domain": task["domain"], "difficulty": lab["difficulty"],
|
| 51 |
+
"strategy": lab["strategy"], "agents": lab["agents"]})
|
| 52 |
+
view = router_view(task["prompt"], task["has_image"], task["est_tokens"])
|
| 53 |
+
return dict(id=task["id"], source=task["source"], domain=task["domain"], kind=task["kind"],
|
| 54 |
+
gold=task["gold"], prompt=task["prompt"], has_image=task["has_image"],
|
| 55 |
+
est_tokens=task["est_tokens"], prompt_truncated=bool(task.get("prompt_truncated")),
|
| 56 |
+
router_input=view, route=target,
|
| 57 |
+
agent_results=json.dumps({a: {k: task["results"][a].get(k) for k in
|
| 58 |
+
("correct", "completion_tokens", "latency", "skipped")}
|
| 59 |
+
for a in AGENTS}),
|
| 60 |
+
messages=[{"role": "system", "content": ROUTER_SYSTEM},
|
| 61 |
+
{"role": "user", "content": view},
|
| 62 |
+
{"role": "assistant", "content": target}])
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def main():
|
| 66 |
+
rows = [json.loads(l) for l in open("data/runs_v3.jsonl")]
|
| 67 |
+
n_all = len(rows)
|
| 68 |
+
rows = [r for r in rows if not any(v.get("error") for v in r["results"].values())]
|
| 69 |
+
print(f"{n_all} runs, {n_all - len(rows)} dropped for transient API errors")
|
| 70 |
+
for r in rows:
|
| 71 |
+
r.setdefault("has_image", False)
|
| 72 |
+
r.setdefault("est_tokens", est_tokens(r["prompt"]))
|
| 73 |
+
for src in {r["source"] for r in rows}:
|
| 74 |
+
sub = [r for r in rows if r["source"] == src]
|
| 75 |
+
for a in AGENTS:
|
| 76 |
+
SOLVE_RATE[src, a] = sum(r["results"][a]["correct"] for r in sub) / len(sub)
|
| 77 |
+
random.shuffle(rows)
|
| 78 |
+
ex = [to_example(r) for r in rows]
|
| 79 |
+
n_test = min(max(200, len(ex) // 10), len(ex) // 5)
|
| 80 |
+
dd = DatasetDict(train=Dataset.from_list(ex[n_test:]), test=Dataset.from_list(ex[:n_test]))
|
| 81 |
+
dd.save_to_disk("data/shaktimaan_routing")
|
| 82 |
+
print(dd)
|
| 83 |
+
routes = Counter(json.loads(e["route"])["strategy"] + ":" + ",".join(json.loads(e["route"])["agents"])
|
| 84 |
+
for e in ex)
|
| 85 |
+
print("label distribution:", *routes.most_common(), sep="\n ")
|
| 86 |
+
for src in sorted({r["source"] for r in rows}):
|
| 87 |
+
sub = [r for r in rows if r["source"] == src]
|
| 88 |
+
print(f"{src:10s} n={len(sub):4d} " + " ".join(
|
| 89 |
+
f"{a}={sum(r['results'][a]['correct'] for r in sub) / len(sub):.2f}" for a in AGENTS))
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
if __name__ == "__main__":
|
| 93 |
+
main()
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- if tools %}
|
| 2 |
+
{{- '<|im_start|>system\n' }}
|
| 3 |
+
{%- if messages[0]['role'] == 'system' %}
|
| 4 |
+
{{- messages[0]['content'] }}
|
| 5 |
+
{%- else %}
|
| 6 |
+
{{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
|
| 7 |
+
{%- endif %}
|
| 8 |
+
{{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
|
| 9 |
+
{%- for tool in tools %}
|
| 10 |
+
{{- "\n" }}
|
| 11 |
+
{{- tool | tojson }}
|
| 12 |
+
{%- endfor %}
|
| 13 |
+
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
|
| 14 |
+
{%- else %}
|
| 15 |
+
{%- if messages[0]['role'] == 'system' %}
|
| 16 |
+
{{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
|
| 17 |
+
{%- else %}
|
| 18 |
+
{{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
|
| 19 |
+
{%- endif %}
|
| 20 |
+
{%- endif %}
|
| 21 |
+
{%- for message in messages %}
|
| 22 |
+
{%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
|
| 23 |
+
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
|
| 24 |
+
{%- elif message.role == "assistant" %}
|
| 25 |
+
{{- '<|im_start|>' + message.role }}
|
| 26 |
+
{%- if message.content %}
|
| 27 |
+
{{- '\n' + message.content }}
|
| 28 |
+
{%- endif %}
|
| 29 |
+
{%- for tool_call in message.tool_calls %}
|
| 30 |
+
{%- if tool_call.function is defined %}
|
| 31 |
+
{%- set tool_call = tool_call.function %}
|
| 32 |
+
{%- endif %}
|
| 33 |
+
{{- '\n<tool_call>\n{"name": "' }}
|
| 34 |
+
{{- tool_call.name }}
|
| 35 |
+
{{- '", "arguments": ' }}
|
| 36 |
+
{{- tool_call.arguments | tojson }}
|
| 37 |
+
{{- '}\n</tool_call>' }}
|
| 38 |
+
{%- endfor %}
|
| 39 |
+
{{- '<|im_end|>\n' }}
|
| 40 |
+
{%- elif message.role == "tool" %}
|
| 41 |
+
{%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
|
| 42 |
+
{{- '<|im_start|>user' }}
|
| 43 |
+
{%- endif %}
|
| 44 |
+
{{- '\n<tool_response>\n' }}
|
| 45 |
+
{{- message.content }}
|
| 46 |
+
{{- '\n</tool_response>' }}
|
| 47 |
+
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
|
| 48 |
+
{{- '<|im_end|>\n' }}
|
| 49 |
+
{%- endif %}
|
| 50 |
+
{%- endif %}
|
| 51 |
+
{%- endfor %}
|
| 52 |
+
{%- if add_generation_prompt %}
|
| 53 |
+
{{- '<|im_start|>assistant\n' }}
|
| 54 |
+
{%- endif %}
|
common.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared config for Shaktimaan: the agent pool, prompts, capabilities and graders."""
|
| 2 |
+
import json, os, re
|
| 3 |
+
|
| 4 |
+
# agent -> served model id, max concurrent requests we send, relative cost per token
|
| 5 |
+
POOL = {
|
| 6 |
+
"gemma-3-1b": dict(model="google/gemma-3-1b-it", concurrency=32, cost=0.02),
|
| 7 |
+
"kimi-vl": dict(model="Kimi-VL-A3B-Thinking", concurrency=64, cost=0.05),
|
| 8 |
+
"nemotron-lightning": dict(model="nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4", concurrency=64, cost=0.10),
|
| 9 |
+
"glm-5.3-flash": dict(model="zai-org/GLM-5.3-Flash", concurrency=32, cost=0.20),
|
| 10 |
+
"qwen3.8-27b": dict(model="Qwen/Qwen3.8-27B", concurrency=12, cost=0.50),
|
| 11 |
+
"nemotron-ultra": dict(model="nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4", concurrency=64, cost=1.00),
|
| 12 |
+
}
|
| 13 |
+
AGENTS = list(POOL) # ordered cheapest -> most expensive
|
| 14 |
+
STRONGEST = "nemotron-ultra"
|
| 15 |
+
CONTEXT = {"gemma-3-1b": 32768, "kimi-vl": 8192, "nemotron-lightning": 131072, "glm-5.3-flash": 32768,
|
| 16 |
+
"qwen3.8-27b": 32768, "nemotron-ultra": 262144}
|
| 17 |
+
VISION = {"kimi-vl"} # only agent served with image input enabled
|
| 18 |
+
MAX_OUT = 8192
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def endpoints() -> dict:
|
| 22 |
+
"""Agent -> OpenAI-compatible base URL (".../v1"), from $SHAKTIMAAN_ENDPOINTS: inline JSON or a path to a JSON file."""
|
| 23 |
+
raw = os.environ["SHAKTIMAAN_ENDPOINTS"]
|
| 24 |
+
return json.load(open(raw)) if os.path.exists(raw) else json.loads(raw)
|
| 25 |
+
|
| 26 |
+
SOLVER_SYSTEM = ("Solve the task. Be concise. End your response with a final line exactly of the form:\n"
|
| 27 |
+
"Final answer: <answer>\nFor multiple-choice questions, the answer is the option letter only.")
|
| 28 |
+
CODE_SYSTEM = ("Write a correct, self-contained Python solution. Put the complete code (imports, required function "
|
| 29 |
+
"names and signatures) in a single ```python code block. Do not include tests or example usage.")
|
| 30 |
+
|
| 31 |
+
ROUTER_SYSTEM = (
|
| 32 |
+
"You are Shaktimaan, an orchestrator. You do not answer the task yourself. You read the task and decide which "
|
| 33 |
+
"agent(s) from the pool should handle it, minimising cost while maximising the chance of a correct answer.\n"
|
| 34 |
+
"Agents (cheapest first): " + ", ".join(AGENTS) + ".\n"
|
| 35 |
+
"Capabilities: only kimi-vl can see images; context limits: nemotron-ultra 262k tokens, nemotron-lightning 131k, "
|
| 36 |
+
"kimi-vl 8k, all others 32k.\n"
|
| 37 |
+
"Strategies: single = one agent answers; ensemble = several agents answer independently and the last one "
|
| 38 |
+
"aggregates.\n"
|
| 39 |
+
"Reply with JSON only: {\"domain\": ..., \"difficulty\": \"easy|medium|hard\", \"strategy\": \"single|ensemble\", "
|
| 40 |
+
"\"agents\": [...]}"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def est_tokens(text: str) -> int:
|
| 45 |
+
"""Conservative token estimate (real tokenizers average ~3.5-4 chars/token on English)."""
|
| 46 |
+
return len(text) // 3 + 1
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def max_out(agent: str, prompt_tokens: int) -> int:
|
| 50 |
+
return min(MAX_OUT, CONTEXT[agent] - prompt_tokens - 256)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def capable(agent: str, prompt: str, has_image: bool) -> bool:
|
| 54 |
+
if has_image and agent not in VISION:
|
| 55 |
+
return False
|
| 56 |
+
return max_out(agent, est_tokens(prompt) + 1500 * has_image) >= 1024
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def truncate(prompt: str, head: int = 5000, tail: int = 1500) -> str:
|
| 60 |
+
return prompt if len(prompt) <= head + tail + 7 else prompt[:head] + "\n[...]\n" + prompt[-tail:]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def router_view(prompt: str, has_image: bool = False, n_tokens: int | None = None) -> str:
|
| 64 |
+
"""What the router reads: a size/attachment header plus the start and end of the task."""
|
| 65 |
+
n = est_tokens(prompt) if n_tokens is None else n_tokens
|
| 66 |
+
return f"[input ~{n // 1000}k tokens{'; 1 image attached' if has_image else ''}]\n" + truncate(prompt)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def strip_think(text: str) -> str:
|
| 70 |
+
text = text or ""
|
| 71 |
+
if "</think>" in text:
|
| 72 |
+
text = text.rsplit("</think>", 1)[1]
|
| 73 |
+
text = re.sub(r"◁think▷.*?◁/think▷", "", text, flags=re.S) # Kimi-VL thinking markers
|
| 74 |
+
return text.strip()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def extract_final(text: str) -> str:
|
| 78 |
+
text = strip_think(text)
|
| 79 |
+
m = re.findall(r"final answer\s*[::]\s*(.+)", text, flags=re.I)
|
| 80 |
+
if m:
|
| 81 |
+
return m[-1].strip()
|
| 82 |
+
m = re.findall(r"^\s*answer\s*[::]\s*(.+)", text, flags=re.I | re.M)
|
| 83 |
+
if m:
|
| 84 |
+
return m[-1].strip()
|
| 85 |
+
boxed = last_boxed(text)
|
| 86 |
+
if boxed is not None:
|
| 87 |
+
return boxed
|
| 88 |
+
lines = [l for l in text.splitlines() if l.strip()]
|
| 89 |
+
return lines[-1].strip() if lines else ""
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def extract_code(text: str) -> str:
|
| 93 |
+
text = strip_think(text)
|
| 94 |
+
blocks = re.findall(r"```(?:python|py)?\s*\n(.*?)```", text, flags=re.S)
|
| 95 |
+
return blocks[-1] if blocks else ""
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def last_boxed(s: str):
|
| 99 |
+
i = s.rfind("\\boxed{")
|
| 100 |
+
if i < 0:
|
| 101 |
+
return None
|
| 102 |
+
i += len("\\boxed{")
|
| 103 |
+
depth, j = 1, i
|
| 104 |
+
while j < len(s) and depth:
|
| 105 |
+
depth += {"{": 1, "}": -1}.get(s[j], 0)
|
| 106 |
+
j += 1
|
| 107 |
+
return s[i:j - 1] if depth == 0 else None
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _norm_math(s: str) -> str:
|
| 111 |
+
s = s.strip().strip("$").strip()
|
| 112 |
+
b = last_boxed(s)
|
| 113 |
+
if b is not None:
|
| 114 |
+
s = b
|
| 115 |
+
for a, b2 in [("\\left", ""), ("\\right", ""), ("\\!", ""), ("\\,", ""), ("\\;", ""), ("dfrac", "frac"),
|
| 116 |
+
("tfrac", "frac"), ("^\\circ", ""), ("^{\\circ}", ""), ("\\%", ""), ("%", ""), ("\\text{", "{"),
|
| 117 |
+
("\\mbox{", "{"), (" ", ""), ("\\$", "")]:
|
| 118 |
+
s = s.replace(a, b2)
|
| 119 |
+
s = re.sub(r"\\frac(\d)(\d)", r"\\frac{\1}{\2}", s)
|
| 120 |
+
s = s.rstrip(".")
|
| 121 |
+
if s.startswith("{") and s.endswith("}") and s.count("{") == 1:
|
| 122 |
+
s = s[1:-1]
|
| 123 |
+
return s
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _num(s: str):
|
| 127 |
+
s = s.replace(",", "").replace("$", "").strip()
|
| 128 |
+
m = re.findall(r"-?\d+(?:\.\d+)?", s)
|
| 129 |
+
return float(m[-1]) if m else None
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def grade(kind: str, pred: str, gold: str) -> bool:
|
| 133 |
+
"""Grades extracted answers. Code is graded by execution (see gen.run_tests), not here."""
|
| 134 |
+
if not pred:
|
| 135 |
+
return False
|
| 136 |
+
if kind == "mc":
|
| 137 |
+
m = re.search(r"\b([A-J])\b", pred.upper().replace("(", " ").replace(")", " "))
|
| 138 |
+
return bool(m) and m.group(1) == gold.upper()
|
| 139 |
+
if kind == "num":
|
| 140 |
+
p, g = _num(pred), _num(gold)
|
| 141 |
+
return p is not None and g is not None and abs(p - g) < 1e-6 * max(1, abs(g))
|
| 142 |
+
if kind == "relaxed": # ChartQA convention: numbers within 5%, otherwise case-insensitive exact match
|
| 143 |
+
pred = last_boxed(pred) or pred
|
| 144 |
+
pred = re.sub(r"\\(?:mathbf|textbf|text|mathrm)\{([^}]*)\}", r"\1", pred).replace("\\(", "").replace("\\)", "")
|
| 145 |
+
p, g = _num(pred), _num(gold)
|
| 146 |
+
if g is not None and p is not None and re.fullmatch(r"[-\d.,%$ ]+", gold.strip()):
|
| 147 |
+
return abs(p - g) <= 0.05 * abs(g) if g else p == 0
|
| 148 |
+
return pred.strip().strip(".").lower() == gold.strip().lower()
|
| 149 |
+
if kind == "math":
|
| 150 |
+
p, g = _norm_math(pred), _norm_math(gold)
|
| 151 |
+
if p == g:
|
| 152 |
+
return True
|
| 153 |
+
try:
|
| 154 |
+
return abs(float(p) - float(g)) < 1e-6
|
| 155 |
+
except ValueError:
|
| 156 |
+
return False
|
| 157 |
+
raise ValueError(kind)
|
config.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Qwen2ForCausalLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_dropout": 0.0,
|
| 6 |
+
"bos_token_id": 151643,
|
| 7 |
+
"dtype": "float32",
|
| 8 |
+
"eos_token_id": 151645,
|
| 9 |
+
"hidden_act": "silu",
|
| 10 |
+
"hidden_size": 896,
|
| 11 |
+
"initializer_range": 0.02,
|
| 12 |
+
"intermediate_size": 4864,
|
| 13 |
+
"layer_types": [
|
| 14 |
+
"full_attention",
|
| 15 |
+
"full_attention",
|
| 16 |
+
"full_attention",
|
| 17 |
+
"full_attention",
|
| 18 |
+
"full_attention",
|
| 19 |
+
"full_attention",
|
| 20 |
+
"full_attention",
|
| 21 |
+
"full_attention",
|
| 22 |
+
"full_attention",
|
| 23 |
+
"full_attention",
|
| 24 |
+
"full_attention",
|
| 25 |
+
"full_attention",
|
| 26 |
+
"full_attention",
|
| 27 |
+
"full_attention",
|
| 28 |
+
"full_attention",
|
| 29 |
+
"full_attention",
|
| 30 |
+
"full_attention",
|
| 31 |
+
"full_attention",
|
| 32 |
+
"full_attention",
|
| 33 |
+
"full_attention",
|
| 34 |
+
"full_attention",
|
| 35 |
+
"full_attention",
|
| 36 |
+
"full_attention",
|
| 37 |
+
"full_attention"
|
| 38 |
+
],
|
| 39 |
+
"max_position_embeddings": 32768,
|
| 40 |
+
"max_window_layers": 21,
|
| 41 |
+
"model_type": "qwen2",
|
| 42 |
+
"num_attention_heads": 14,
|
| 43 |
+
"num_hidden_layers": 24,
|
| 44 |
+
"num_key_value_heads": 2,
|
| 45 |
+
"pad_token_id": 151643,
|
| 46 |
+
"rms_norm_eps": 1e-06,
|
| 47 |
+
"rope_parameters": {
|
| 48 |
+
"rope_theta": 1000000.0,
|
| 49 |
+
"rope_type": "default"
|
| 50 |
+
},
|
| 51 |
+
"sliding_window": null,
|
| 52 |
+
"tie_word_embeddings": true,
|
| 53 |
+
"transformers_version": "5.19.0",
|
| 54 |
+
"use_cache": false,
|
| 55 |
+
"use_sliding_window": false,
|
| 56 |
+
"vocab_size": 151936
|
| 57 |
+
}
|
eval_results.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"n_test": 554,
|
| 3 |
+
"valid_json": 1.0,
|
| 4 |
+
"route_match": 0.5306859205776173,
|
| 5 |
+
"live_ensembles": 31,
|
| 6 |
+
"route_counts": {
|
| 7 |
+
"nemotron-lightning": 218,
|
| 8 |
+
"glm-5.3-flash": 192,
|
| 9 |
+
"kimi-vl": 80,
|
| 10 |
+
"glm-5.3-flash,qwen3.8-27b,nemotron-ultra": 32,
|
| 11 |
+
"nemotron-ultra": 31,
|
| 12 |
+
"qwen3.8-27b": 1
|
| 13 |
+
},
|
| 14 |
+
"policies": {
|
| 15 |
+
"shaktimaan": {
|
| 16 |
+
"accuracy": 0.8339,
|
| 17 |
+
"relative_cost": 0.424
|
| 18 |
+
},
|
| 19 |
+
"oracle": {
|
| 20 |
+
"accuracy": 0.8971,
|
| 21 |
+
"relative_cost": 0.49
|
| 22 |
+
},
|
| 23 |
+
"always:gemma-3-1b": {
|
| 24 |
+
"accuracy": 0.2563,
|
| 25 |
+
"relative_cost": 0.0024
|
| 26 |
+
},
|
| 27 |
+
"always:kimi-vl": {
|
| 28 |
+
"accuracy": 0.6137,
|
| 29 |
+
"relative_cost": 0.0736
|
| 30 |
+
},
|
| 31 |
+
"always:nemotron-lightning": {
|
| 32 |
+
"accuracy": 0.7563,
|
| 33 |
+
"relative_cost": 0.1089
|
| 34 |
+
},
|
| 35 |
+
"always:glm-5.3-flash": {
|
| 36 |
+
"accuracy": 0.7924,
|
| 37 |
+
"relative_cost": 0.0944
|
| 38 |
+
},
|
| 39 |
+
"always:qwen3.8-27b": {
|
| 40 |
+
"accuracy": 0.7888,
|
| 41 |
+
"relative_cost": 0.3641
|
| 42 |
+
},
|
| 43 |
+
"always:nemotron-ultra": {
|
| 44 |
+
"accuracy": 0.7852,
|
| 45 |
+
"relative_cost": 1.0
|
| 46 |
+
},
|
| 47 |
+
"rule:glm-5.3-flash": {
|
| 48 |
+
"accuracy": 0.8574,
|
| 49 |
+
"relative_cost": 0.1331
|
| 50 |
+
},
|
| 51 |
+
"rule:qwen3.8-27b": {
|
| 52 |
+
"accuracy": 0.8538,
|
| 53 |
+
"relative_cost": 0.4028
|
| 54 |
+
},
|
| 55 |
+
"rule:nemotron-ultra": {
|
| 56 |
+
"accuracy": 0.8267,
|
| 57 |
+
"relative_cost": 1.0009
|
| 58 |
+
}
|
| 59 |
+
},
|
| 60 |
+
"accuracy_by_source": {
|
| 61 |
+
"arc": {
|
| 62 |
+
"n": 75,
|
| 63 |
+
"shaktimaan": 0.987,
|
| 64 |
+
"oracle": 1.0,
|
| 65 |
+
"always:nemotron-ultra": 0.987,
|
| 66 |
+
"rule:glm-5.3-flash": 0.987,
|
| 67 |
+
"rule:qwen3.8-27b": 0.987
|
| 68 |
+
},
|
| 69 |
+
"chartqa": {
|
| 70 |
+
"n": 31,
|
| 71 |
+
"shaktimaan": 0.742,
|
| 72 |
+
"oracle": 0.742,
|
| 73 |
+
"always:nemotron-ultra": 0.0,
|
| 74 |
+
"rule:glm-5.3-flash": 0.742,
|
| 75 |
+
"rule:qwen3.8-27b": 0.742
|
| 76 |
+
},
|
| 77 |
+
"gsm8k": {
|
| 78 |
+
"n": 105,
|
| 79 |
+
"shaktimaan": 0.895,
|
| 80 |
+
"oracle": 0.981,
|
| 81 |
+
"always:nemotron-ultra": 0.971,
|
| 82 |
+
"rule:glm-5.3-flash": 0.962,
|
| 83 |
+
"rule:qwen3.8-27b": 0.981
|
| 84 |
+
},
|
| 85 |
+
"humaneval": {
|
| 86 |
+
"n": 18,
|
| 87 |
+
"shaktimaan": 0.889,
|
| 88 |
+
"oracle": 1.0,
|
| 89 |
+
"always:nemotron-ultra": 0.944,
|
| 90 |
+
"rule:glm-5.3-flash": 0.944,
|
| 91 |
+
"rule:qwen3.8-27b": 0.944
|
| 92 |
+
},
|
| 93 |
+
"longbench": {
|
| 94 |
+
"n": 32,
|
| 95 |
+
"shaktimaan": 0.594,
|
| 96 |
+
"oracle": 0.594,
|
| 97 |
+
"always:nemotron-ultra": 0.594,
|
| 98 |
+
"rule:glm-5.3-flash": 0.469,
|
| 99 |
+
"rule:qwen3.8-27b": 0.5
|
| 100 |
+
},
|
| 101 |
+
"math": {
|
| 102 |
+
"n": 106,
|
| 103 |
+
"shaktimaan": 0.755,
|
| 104 |
+
"oracle": 0.821,
|
| 105 |
+
"always:nemotron-ultra": 0.651,
|
| 106 |
+
"rule:glm-5.3-flash": 0.745,
|
| 107 |
+
"rule:qwen3.8-27b": 0.755
|
| 108 |
+
},
|
| 109 |
+
"mbpp": {
|
| 110 |
+
"n": 67,
|
| 111 |
+
"shaktimaan": 0.94,
|
| 112 |
+
"oracle": 1.0,
|
| 113 |
+
"always:nemotron-ultra": 0.97,
|
| 114 |
+
"rule:glm-5.3-flash": 0.985,
|
| 115 |
+
"rule:qwen3.8-27b": 0.925
|
| 116 |
+
},
|
| 117 |
+
"mmlu_pro": {
|
| 118 |
+
"n": 120,
|
| 119 |
+
"shaktimaan": 0.775,
|
| 120 |
+
"oracle": 0.875,
|
| 121 |
+
"always:nemotron-ultra": 0.742,
|
| 122 |
+
"rule:glm-5.3-flash": 0.833,
|
| 123 |
+
"rule:qwen3.8-27b": 0.817
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
"live_ensembles_solved": 11
|
| 127 |
+
}
|
evaluate.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 4: score the router on held-out tasks.
|
| 2 |
+
|
| 3 |
+
Single-agent routes are scored from the recorded agent outcomes (an agent that cannot take a task - no image input,
|
| 4 |
+
context too small - counts as a failure at zero cost). Shaktimaan's ensemble routes on text/code tasks are executed
|
| 5 |
+
live (agents + aggregator via their OpenAI-compatible endpoints) and graded; oracle ensemble labels are scored conservatively as
|
| 6 |
+
the aggregator's recorded outcome.
|
| 7 |
+
|
| 8 |
+
Baselines: every always-one-agent policy, plus capability rules ("rule:X" = image -> kimi-vl, too long for X ->
|
| 9 |
+
the cheapest agent with enough context, otherwise X). A useful router has to beat the rules, not just fixed agents.
|
| 10 |
+
"""
|
| 11 |
+
import asyncio, json, sys
|
| 12 |
+
from collections import Counter, defaultdict
|
| 13 |
+
import httpx
|
| 14 |
+
import torch
|
| 15 |
+
from datasets import load_from_disk
|
| 16 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 17 |
+
from common import (AGENTS, CODE_SYSTEM, POOL, ROUTER_SYSTEM, SOLVER_SYSTEM, STRONGEST, capable, endpoints,
|
| 18 |
+
extract_code, extract_final, grade, strip_think)
|
| 19 |
+
from gen import call, run_tests
|
| 20 |
+
|
| 21 |
+
path = sys.argv[1] if len(sys.argv) > 1 else "shaktimaan-0.5b"
|
| 22 |
+
tok = AutoTokenizer.from_pretrained(path)
|
| 23 |
+
tok.padding_side = "left"
|
| 24 |
+
model = AutoModelForCausalLM.from_pretrained(path, dtype=torch.bfloat16).cuda().eval()
|
| 25 |
+
test = load_from_disk("data/shaktimaan_routing")["test"]
|
| 26 |
+
BY_CONTEXT = ["kimi-vl", "glm-5.3-flash", "nemotron-lightning", STRONGEST] # cheapest first, growing context
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def parse(text):
|
| 30 |
+
try:
|
| 31 |
+
r = json.loads(text.strip())
|
| 32 |
+
agents = [a for a in r.get("agents", []) if a in POOL]
|
| 33 |
+
return r, agents or [STRONGEST], True
|
| 34 |
+
except Exception: # noqa: BLE001
|
| 35 |
+
return {}, [STRONGEST], False
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def score(row, agents):
|
| 39 |
+
res = json.loads(row["agent_results"])
|
| 40 |
+
return bool(res[agents[-1]]["correct"]), sum(POOL[a]["cost"] * res[a]["completion_tokens"] for a in agents)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def rule(row, default):
|
| 44 |
+
res = json.loads(row["agent_results"])
|
| 45 |
+
if row["has_image"]:
|
| 46 |
+
return ["kimi-vl"]
|
| 47 |
+
if not res[default]["skipped"]:
|
| 48 |
+
return [default]
|
| 49 |
+
return [next((a for a in BY_CONTEXT if not res[a]["skipped"]), STRONGEST)]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
async def run_ensembles(jobs):
|
| 53 |
+
"""jobs: list of (row, agents). Returns [(correct, cost)] from real agent + aggregator calls."""
|
| 54 |
+
urls = {a: f"{base.rstrip('/')}/chat/completions" for a, base in endpoints().items()}
|
| 55 |
+
sems = {a: asyncio.Semaphore(POOL[a]["concurrency"]) for a in POOL}
|
| 56 |
+
|
| 57 |
+
async def one(client, row, agents):
|
| 58 |
+
code = row["kind"] == "code"
|
| 59 |
+
system = CODE_SYSTEM if code else SOLVER_SYSTEM
|
| 60 |
+
msgs = [{"role": "system", "content": system}, {"role": "user", "content": row["prompt"]}]
|
| 61 |
+
drafts = await asyncio.gather(*[call(client, urls[a], POOL[a]["model"], sems[a], msgs) for a in agents])
|
| 62 |
+
joined = "\n\n".join(f"### Candidate {i + 1}\n{strip_think(d['text'])}" for i, d in enumerate(drafts))
|
| 63 |
+
agg = await call(client, urls[agents[-1]], POOL[agents[-1]]["model"], sems[agents[-1]], [
|
| 64 |
+
{"role": "system", "content": system},
|
| 65 |
+
{"role": "user", "content": f"{row['prompt']}\n\nIndependent candidate solutions:\n{joined}\n\n"
|
| 66 |
+
"Check them, resolve disagreements, and give the best final answer."}])
|
| 67 |
+
cost = sum(POOL[a]["cost"] * d["completion_tokens"] for a, d in zip(agents, drafts))
|
| 68 |
+
cost += POOL[agents[-1]]["cost"] * agg["completion_tokens"]
|
| 69 |
+
ok = await run_tests(extract_code(agg["text"]), row["gold"]) if code else \
|
| 70 |
+
grade(row["kind"], extract_final(agg["text"]), row["gold"])
|
| 71 |
+
return ok, cost
|
| 72 |
+
|
| 73 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(1800)) as client:
|
| 74 |
+
return await asyncio.gather(*[one(client, r, a) for r, a in jobs])
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
preds = []
|
| 78 |
+
for i in range(0, len(test), 32):
|
| 79 |
+
batch = test[i:i + 32]
|
| 80 |
+
msgs = [[{"role": "system", "content": ROUTER_SYSTEM}, {"role": "user", "content": v}]
|
| 81 |
+
for v in batch["router_input"]]
|
| 82 |
+
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", padding=True,
|
| 83 |
+
return_dict=True).to("cuda")
|
| 84 |
+
with torch.no_grad():
|
| 85 |
+
out = model.generate(**enc, max_new_tokens=96, do_sample=False)
|
| 86 |
+
preds += tok.batch_decode(out[:, enc["input_ids"].shape[1]:], skip_special_tokens=True)
|
| 87 |
+
|
| 88 |
+
parsed = [parse(p) for p in preds]
|
| 89 |
+
ens = [(row, [a for a in ag if capable(a, row["prompt"], False)])
|
| 90 |
+
for row, (r, ag, ok) in zip(test, parsed)
|
| 91 |
+
if r.get("strategy") == "ensemble" and len(ag) > 1 and not row["has_image"] and not row["prompt_truncated"]]
|
| 92 |
+
ens = [(row, ag) for row, ag in ens if len(ag) > 1]
|
| 93 |
+
ens_results = dict(zip([row["id"] for row, _ in ens], asyncio.run(run_ensembles(ens))))
|
| 94 |
+
print(f"ran {len(ens)} live ensembles, solved {sum(c for c, _ in ens_results.values())}", flush=True)
|
| 95 |
+
|
| 96 |
+
policies = ["shaktimaan", "oracle"] + [f"always:{a}" for a in AGENTS] + \
|
| 97 |
+
["rule:glm-5.3-flash", "rule:qwen3.8-27b", f"rule:{STRONGEST}"]
|
| 98 |
+
stats = {k: [0, 0.0] for k in policies}
|
| 99 |
+
by_src = defaultdict(lambda: defaultdict(list))
|
| 100 |
+
valid = exact = 0
|
| 101 |
+
for row, (r, agents, ok) in zip(test, parsed):
|
| 102 |
+
valid += ok
|
| 103 |
+
exact += ok and agents == json.loads(row["route"])["agents"]
|
| 104 |
+
choices = {"shaktimaan": None, "oracle": json.loads(row["route"])["agents"],
|
| 105 |
+
**{f"always:{a}": [a] for a in AGENTS},
|
| 106 |
+
**{f"rule:{a}": rule(row, a) for a in ["glm-5.3-flash", "qwen3.8-27b", STRONGEST]}}
|
| 107 |
+
for key, ag in choices.items():
|
| 108 |
+
if key == "shaktimaan":
|
| 109 |
+
c, cost = ens_results[row["id"]] if row["id"] in ens_results else score(row, agents)
|
| 110 |
+
else:
|
| 111 |
+
c, cost = score(row, ag)
|
| 112 |
+
stats[key][0] += c
|
| 113 |
+
stats[key][1] += cost
|
| 114 |
+
by_src[row["source"]][key].append(c)
|
| 115 |
+
|
| 116 |
+
n = len(test)
|
| 117 |
+
base_cost = stats[f"always:{STRONGEST}"][1]
|
| 118 |
+
report = {"n_test": n, "valid_json": valid / n, "route_match": exact / n, "live_ensembles": len(ens),
|
| 119 |
+
"live_ensembles_solved": sum(c for c, _ in ens_results.values()),
|
| 120 |
+
"route_counts": dict(Counter(",".join(ag) for _, ag, _ in parsed).most_common()),
|
| 121 |
+
"policies": {k: {"accuracy": round(v[0] / n, 4), "relative_cost": round(v[1] / base_cost, 4)}
|
| 122 |
+
for k, v in stats.items()},
|
| 123 |
+
"accuracy_by_source": {s: {"n": len(d["shaktimaan"]),
|
| 124 |
+
**{k: round(sum(v) / len(v), 3) for k, v in d.items()
|
| 125 |
+
if k in ("shaktimaan", "oracle", "rule:qwen3.8-27b", "rule:glm-5.3-flash",
|
| 126 |
+
f"always:{STRONGEST}")}}
|
| 127 |
+
for s, d in sorted(by_src.items())}}
|
| 128 |
+
print(json.dumps(report, indent=2))
|
| 129 |
+
json.dump(report, open(f"{path}/eval_results.json", "w"), indent=2)
|
gen.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 1: build a diverse task set with checkable answers and run every capable agent in the pool on it.
|
| 2 |
+
|
| 3 |
+
Sources: GSM8K, MATH, MMLU-Pro, ARC (text reasoning), MBPP + HumanEval (code, graded by running the tests in a
|
| 4 |
+
no-network Docker sandbox), ChartQA (images), LongBench-v2 (long context, up to ~260k tokens).
|
| 5 |
+
Agents that cannot take a task (no image input, context too small) are recorded as failures without being called.
|
| 6 |
+
Reuses results already in data/runs.jsonl and only runs missing (task, agent) pairs. Writes data/runs_v3.jsonl.
|
| 7 |
+
"""
|
| 8 |
+
import asyncio, base64, io, json, os, random, sys, time
|
| 9 |
+
import httpx
|
| 10 |
+
from datasets import load_dataset
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
from common import (AGENTS, CODE_SYSTEM, POOL, SOLVER_SYSTEM, VISION, capable, endpoints, est_tokens, extract_code,
|
| 13 |
+
extract_final, grade, max_out, truncate)
|
| 14 |
+
|
| 15 |
+
OLD, OUT = "data/runs.jsonl", os.environ.get("OUT", "data/runs_v3.jsonl")
|
| 16 |
+
SIZES = {"gsm8k": 1000, "math": 1000, "mmlu_pro": 1200, "arc": 800, "mbpp": 700, "chartqa": 300}
|
| 17 |
+
LONG_TOKENS = 32768 # prompts above this get their own small concurrency budget
|
| 18 |
+
random.seed(0)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def load_tasks():
|
| 22 |
+
tasks = []
|
| 23 |
+
# --- text reasoning (identical to v1/v2 so existing results are reused by id) ---
|
| 24 |
+
ds = load_dataset("openai/gsm8k", "main", split="train").shuffle(seed=0).select(range(SIZES["gsm8k"]))
|
| 25 |
+
for i, r in enumerate(ds):
|
| 26 |
+
tasks.append(dict(id=f"gsm8k-{i}", source="gsm8k", domain="math", kind="num",
|
| 27 |
+
prompt=r["question"], gold=r["answer"].split("####")[-1].strip()))
|
| 28 |
+
rows = []
|
| 29 |
+
for cfg in ["algebra", "counting_and_probability", "geometry", "intermediate_algebra",
|
| 30 |
+
"number_theory", "prealgebra", "precalculus"]:
|
| 31 |
+
rows += list(load_dataset("EleutherAI/hendrycks_math", cfg, split="train"))
|
| 32 |
+
from common import last_boxed
|
| 33 |
+
rows = [r for r in rows if last_boxed(r["solution"])]
|
| 34 |
+
random.shuffle(rows)
|
| 35 |
+
for i, r in enumerate(rows[:SIZES["math"]]):
|
| 36 |
+
tasks.append(dict(id=f"math-{i}", source="math", domain="math", kind="math", level=r["level"],
|
| 37 |
+
prompt=r["problem"], gold=last_boxed(r["solution"])))
|
| 38 |
+
ds = load_dataset("TIGER-Lab/MMLU-Pro", split="test").shuffle(seed=0).select(range(SIZES["mmlu_pro"]))
|
| 39 |
+
for i, r in enumerate(ds):
|
| 40 |
+
opts = "\n".join(f"{chr(65 + k)}. {o}" for k, o in enumerate(r["options"]))
|
| 41 |
+
tasks.append(dict(id=f"mmlupro-{i}", source="mmlu_pro", domain=r["category"], kind="mc",
|
| 42 |
+
prompt=f"{r['question']}\n\n{opts}", gold=r["answer"]))
|
| 43 |
+
ds = load_dataset("allenai/ai2_arc", "ARC-Challenge", split="train").shuffle(seed=0)
|
| 44 |
+
ds = ds.select(range(min(SIZES["arc"], len(ds))))
|
| 45 |
+
for i, r in enumerate(ds):
|
| 46 |
+
labels = r["choices"]["label"]
|
| 47 |
+
opts = "\n".join(f"{chr(65 + k)}. {t}" for k, t in enumerate(r["choices"]["text"]))
|
| 48 |
+
gold = chr(65 + labels.index(r["answerKey"]))
|
| 49 |
+
tasks.append(dict(id=f"arc-{i}", source="arc", domain="science", kind="mc",
|
| 50 |
+
prompt=f"{r['question']}\n\n{opts}", gold=gold))
|
| 51 |
+
# --- code ---
|
| 52 |
+
mbpp = []
|
| 53 |
+
for split in ["train", "test", "validation", "prompt"]:
|
| 54 |
+
mbpp += list(load_dataset("google-research-datasets/mbpp", "full", split=split))
|
| 55 |
+
random.Random(2).shuffle(mbpp)
|
| 56 |
+
for r in mbpp[:SIZES["mbpp"]]:
|
| 57 |
+
tasks.append(dict(id=f"mbpp-{r['task_id']}", source="mbpp", domain="code", kind="code",
|
| 58 |
+
prompt=f"{r['text']}\nYour code should pass these tests:\n" + "\n".join(r["test_list"]),
|
| 59 |
+
gold=(r["test_setup_code"] or "") + "\n" + "\n".join(r["test_list"])))
|
| 60 |
+
for r in load_dataset("openai/openai_humaneval", split="test"):
|
| 61 |
+
tasks.append(dict(id=f"humaneval-{r['task_id'].split('/')[-1]}", source="humaneval", domain="code",
|
| 62 |
+
kind="code", prompt=f"Complete the following Python function:\n```python\n{r['prompt']}```",
|
| 63 |
+
gold=f"{r['test']}\ncheck({r['entry_point']})"))
|
| 64 |
+
# --- vision ---
|
| 65 |
+
ds = load_dataset("HuggingFaceM4/ChartQA", split="test").shuffle(seed=0).select(range(SIZES["chartqa"]))
|
| 66 |
+
for i, r in enumerate(ds):
|
| 67 |
+
buf = io.BytesIO()
|
| 68 |
+
r["image"].convert("RGB").save(buf, "PNG")
|
| 69 |
+
tasks.append(dict(id=f"chartqa-{i}", source="chartqa", domain="vision", kind="relaxed", has_image=True,
|
| 70 |
+
image="data:image/png;base64," + base64.b64encode(buf.getvalue()).decode(),
|
| 71 |
+
prompt=r["query"], gold=str(r["label"][0])))
|
| 72 |
+
# --- long context ---
|
| 73 |
+
for r in load_dataset("THUDM/LongBench-v2", split="train"):
|
| 74 |
+
prompt = (f"Read the following text and answer the question.\n\n{r['context']}\n\nQuestion: {r['question']}\n"
|
| 75 |
+
f"A. {r['choice_A']}\nB. {r['choice_B']}\nC. {r['choice_C']}\nD. {r['choice_D']}")
|
| 76 |
+
if any(capable(a, prompt, False) for a in AGENTS):
|
| 77 |
+
tasks.append(dict(id=f"longbench-{r['_id']}", source="longbench", domain=f"long:{r['domain']}",
|
| 78 |
+
kind="mc", prompt=prompt, gold=r["answer"]))
|
| 79 |
+
for t in tasks:
|
| 80 |
+
t.setdefault("has_image", False)
|
| 81 |
+
t["est_tokens"] = est_tokens(t["prompt"])
|
| 82 |
+
return tasks
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
async def call(client, url, model, sem, messages, max_tokens=8192):
|
| 86 |
+
"""POST a chat completion. HTTP 400 (e.g. context overflow) is returned immediately as `rejected`."""
|
| 87 |
+
if isinstance(messages, str):
|
| 88 |
+
messages = [{"role": "system", "content": SOLVER_SYSTEM}, {"role": "user", "content": messages}]
|
| 89 |
+
async with sem:
|
| 90 |
+
t0, err = time.time(), None
|
| 91 |
+
for attempt in range(4):
|
| 92 |
+
try:
|
| 93 |
+
r = await client.post(url, json={"model": model, "max_tokens": max_tokens, "temperature": 0.6,
|
| 94 |
+
"messages": messages})
|
| 95 |
+
if r.status_code == 400:
|
| 96 |
+
return dict(text="", rejected=r.text[:200], prompt_tokens=0, completion_tokens=0,
|
| 97 |
+
latency=time.time() - t0)
|
| 98 |
+
r.raise_for_status()
|
| 99 |
+
j = r.json()
|
| 100 |
+
return dict(text=j["choices"][0]["message"].get("content") or "",
|
| 101 |
+
finish=j["choices"][0].get("finish_reason"),
|
| 102 |
+
prompt_tokens=j["usage"]["prompt_tokens"],
|
| 103 |
+
completion_tokens=j["usage"]["completion_tokens"], latency=time.time() - t0)
|
| 104 |
+
except Exception as e: # noqa: BLE001
|
| 105 |
+
err = repr(e)[:200]
|
| 106 |
+
await asyncio.sleep(2 ** attempt)
|
| 107 |
+
return dict(text="", error=err, prompt_tokens=0, completion_tokens=0, latency=time.time() - t0)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
SANDBOX = asyncio.Semaphore(24)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
async def run_tests(code: str, tests: str) -> bool:
|
| 114 |
+
"""Run model code + tests in a throwaway container: no network, 512 MB RAM, 1 CPU, 15 s."""
|
| 115 |
+
if not code.strip():
|
| 116 |
+
return False
|
| 117 |
+
async with SANDBOX:
|
| 118 |
+
proc = await asyncio.create_subprocess_exec(
|
| 119 |
+
"docker", "run", "--rm", "-i", "--network", "none", "--memory", "512m", "--cpus", "1",
|
| 120 |
+
"--pids-limit", "64", "--read-only", "--tmpfs", "/tmp:size=64m", "python:3.12-slim",
|
| 121 |
+
"timeout", "15", "python", "-", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.DEVNULL,
|
| 122 |
+
stderr=asyncio.subprocess.DEVNULL)
|
| 123 |
+
try:
|
| 124 |
+
await asyncio.wait_for(proc.communicate((code + "\n\n" + tests).encode()), timeout=60)
|
| 125 |
+
except asyncio.TimeoutError:
|
| 126 |
+
proc.kill()
|
| 127 |
+
return False
|
| 128 |
+
return proc.returncode == 0
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def messages_for(task):
|
| 132 |
+
if task["kind"] == "code":
|
| 133 |
+
return [{"role": "system", "content": CODE_SYSTEM}, {"role": "user", "content": task["prompt"]}]
|
| 134 |
+
user = task["prompt"]
|
| 135 |
+
if task["has_image"]:
|
| 136 |
+
user = [{"type": "image_url", "image_url": {"url": task["image"]}}, {"type": "text", "text": task["prompt"]}]
|
| 137 |
+
return [{"role": "system", "content": SOLVER_SYSTEM}, {"role": "user", "content": user}]
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
async def run_agent(client, urls, sems, task, agent):
|
| 141 |
+
if not capable(agent, task["prompt"], task["has_image"]):
|
| 142 |
+
reason = "no_vision" if task["has_image"] and agent not in VISION else "context"
|
| 143 |
+
return dict(pred="", correct=False, prompt_tokens=0, completion_tokens=0, latency=0, finish=None,
|
| 144 |
+
error=None, skipped=reason)
|
| 145 |
+
sem = sems["long" if task["est_tokens"] > LONG_TOKENS else "short"][agent]
|
| 146 |
+
o = await call(client, urls[agent], POOL[agent]["model"], sem, messages_for(task),
|
| 147 |
+
max_out(agent, task["est_tokens"] + 1500 * task["has_image"]))
|
| 148 |
+
if task["kind"] == "code":
|
| 149 |
+
code = extract_code(o["text"])
|
| 150 |
+
pred, correct = code[:300], await run_tests(code, task["gold"])
|
| 151 |
+
else:
|
| 152 |
+
pred = extract_final(o["text"])
|
| 153 |
+
correct = grade(task["kind"], pred, task["gold"])
|
| 154 |
+
return dict(pred=pred[:300], correct=correct, **{k: o[k] for k in ("prompt_tokens", "completion_tokens",
|
| 155 |
+
"latency")},
|
| 156 |
+
finish=o.get("finish"), error=o.get("error"), skipped="rejected" if o.get("rejected") else None)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
async def run_task(client, urls, sems, task, prior):
|
| 160 |
+
todo = [a for a in AGENTS if a not in prior]
|
| 161 |
+
outs = await asyncio.gather(*[run_agent(client, urls, sems, task, a) for a in todo])
|
| 162 |
+
task = {k: v for k, v in task.items() if k != "image"}
|
| 163 |
+
if task["source"] == "longbench": # keep the dataset small: store only what the router reads of huge contexts
|
| 164 |
+
task["prompt"] = truncate(task["prompt"])
|
| 165 |
+
task["prompt_truncated"] = True
|
| 166 |
+
task["results"] = {**prior, **dict(zip(todo, outs))}
|
| 167 |
+
return task
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
async def main():
|
| 171 |
+
os.makedirs("data", exist_ok=True)
|
| 172 |
+
tasks = load_tasks()
|
| 173 |
+
old = {}
|
| 174 |
+
if os.path.exists(OLD):
|
| 175 |
+
old = {r["id"]: r["results"] for r in map(json.loads, open(OLD))
|
| 176 |
+
if not any(v.get("error") for v in r["results"].values())}
|
| 177 |
+
done = {json.loads(l)["id"] for l in open(OUT)} if os.path.exists(OUT) else set()
|
| 178 |
+
todo = [t for t in tasks if t["id"] not in done]
|
| 179 |
+
if os.environ.get("SOURCES"):
|
| 180 |
+
todo = [t for t in todo if t["source"] in os.environ["SOURCES"].split(",")]
|
| 181 |
+
if os.environ.get("LIMIT"):
|
| 182 |
+
todo = random.Random(1).sample(todo, int(os.environ["LIMIT"]))
|
| 183 |
+
print(f"{len(tasks)} tasks ({sum(t['source'] == 'longbench' for t in tasks)} long), {len(done)} done, "
|
| 184 |
+
f"{len(todo)} to run, {sum(t['id'] in old for t in todo)} reuse prior results", flush=True)
|
| 185 |
+
urls = {n: f"{base.rstrip('/')}/chat/completions" for n, base in endpoints().items()}
|
| 186 |
+
sems = {"short": {n: asyncio.Semaphore(POOL[n]["concurrency"]) for n in POOL},
|
| 187 |
+
"long": {n: asyncio.Semaphore(4) for n in POOL}}
|
| 188 |
+
limits = httpx.Limits(max_connections=400, max_keepalive_connections=400)
|
| 189 |
+
async with httpx.AsyncClient(timeout=httpx.Timeout(1800), limits=limits) as client:
|
| 190 |
+
with open(OUT, "a") as f:
|
| 191 |
+
jobs = [run_task(client, urls, sems, t, old.get(t["id"], {})) for t in todo]
|
| 192 |
+
for fut in tqdm(asyncio.as_completed(jobs), total=len(jobs), mininterval=30, file=sys.stdout):
|
| 193 |
+
f.write(json.dumps(await fut) + "\n")
|
| 194 |
+
f.flush()
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
if __name__ == "__main__":
|
| 198 |
+
asyncio.run(main())
|
generation_config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token_id": 151643,
|
| 3 |
+
"do_sample": true,
|
| 4 |
+
"eos_token_id": [
|
| 5 |
+
151645,
|
| 6 |
+
151643
|
| 7 |
+
],
|
| 8 |
+
"pad_token_id": 151643,
|
| 9 |
+
"repetition_penalty": 1.1,
|
| 10 |
+
"temperature": 0.7,
|
| 11 |
+
"top_k": 20,
|
| 12 |
+
"top_p": 0.8,
|
| 13 |
+
"transformers_version": "5.19.0"
|
| 14 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7e9af94dfa0109e0d3fc82dc339210890b3eeaa2c9261a2adf84c202a71d7805
|
| 3 |
+
size 1976163472
|
orchestrate.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shaktimaan runtime: route a task with the 0.5B router, then call the chosen agent(s) via OpenAI-compatible APIs.
|
| 2 |
+
|
| 3 |
+
Usage:
|
| 4 |
+
SHAKTIMAAN_ENDPOINTS='{"glm-5.3-flash": "http://host:8000/v1", ...}' \
|
| 5 |
+
python orchestrate.py [--image chart.png] [--code] "task"
|
| 6 |
+
"""
|
| 7 |
+
import argparse, asyncio, base64, json, mimetypes, os, sys
|
| 8 |
+
import httpx
|
| 9 |
+
import torch
|
| 10 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 11 |
+
from common import (CODE_SYSTEM, POOL, ROUTER_SYSTEM, SOLVER_SYSTEM, STRONGEST, capable, endpoints, est_tokens,
|
| 12 |
+
max_out, router_view, strip_think)
|
| 13 |
+
|
| 14 |
+
ROUTER = os.environ.get("SHAKTIMAAN_ROUTER", "podstack/Shaktimaan")
|
| 15 |
+
ENDPOINTS = endpoints()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def route(task, has_image):
|
| 19 |
+
tok = AutoTokenizer.from_pretrained(ROUTER)
|
| 20 |
+
model = AutoModelForCausalLM.from_pretrained(ROUTER, dtype=torch.bfloat16)
|
| 21 |
+
enc = tok.apply_chat_template([{"role": "system", "content": ROUTER_SYSTEM},
|
| 22 |
+
{"role": "user", "content": router_view(task, has_image)}],
|
| 23 |
+
add_generation_prompt=True, return_tensors="pt", return_dict=True)
|
| 24 |
+
out = model.generate(**enc, max_new_tokens=96, do_sample=False)
|
| 25 |
+
try:
|
| 26 |
+
plan = json.loads(tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True))
|
| 27 |
+
except Exception: # noqa: BLE001
|
| 28 |
+
plan = {"strategy": "single", "agents": []}
|
| 29 |
+
# keep only agents that are reachable and can actually take this input
|
| 30 |
+
ok = [a for a in plan.get("agents", []) if a in ENDPOINTS and capable(a, task, has_image)]
|
| 31 |
+
plan["agents"] = ok or [a for a in [STRONGEST, "kimi-vl"] if a in ENDPOINTS and capable(a, task, has_image)][:1]
|
| 32 |
+
return plan
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
async def ask(client, agent, messages, n_tokens):
|
| 36 |
+
r = await client.post(f"{ENDPOINTS[agent]}/chat/completions",
|
| 37 |
+
json={"model": POOL[agent]["model"], "messages": messages,
|
| 38 |
+
"max_tokens": max_out(agent, n_tokens)})
|
| 39 |
+
r.raise_for_status()
|
| 40 |
+
return strip_think(r.json()["choices"][0]["message"]["content"])
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
async def run(task, image=None, code=False):
|
| 44 |
+
has_image = image is not None
|
| 45 |
+
plan = route(task, has_image)
|
| 46 |
+
print("route:", json.dumps(plan), file=sys.stderr)
|
| 47 |
+
system = CODE_SYSTEM if code else SOLVER_SYSTEM
|
| 48 |
+
user = [{"type": "image_url", "image_url": {"url": image}}, {"type": "text", "text": task}] if has_image else task
|
| 49 |
+
msgs = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 50 |
+
n = est_tokens(task) + 1500 * has_image
|
| 51 |
+
async with httpx.AsyncClient(timeout=1800) as client:
|
| 52 |
+
if plan.get("strategy") != "ensemble" or len(plan["agents"]) == 1:
|
| 53 |
+
return await ask(client, plan["agents"][0], msgs, n)
|
| 54 |
+
drafts = await asyncio.gather(*[ask(client, a, msgs, n) for a in plan["agents"]])
|
| 55 |
+
joined = "\n\n".join(f"### Candidate {i + 1}\n{d}" for i, d in enumerate(drafts))
|
| 56 |
+
agg = f"{task}\n\nIndependent candidate solutions:\n{joined}\n\n" \
|
| 57 |
+
"Check them, resolve disagreements, and give the best final answer."
|
| 58 |
+
return await ask(client, plan["agents"][-1], [{"role": "system", "content": system},
|
| 59 |
+
{"role": "user", "content": agg}], est_tokens(agg))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
p = argparse.ArgumentParser()
|
| 64 |
+
p.add_argument("task", nargs="+")
|
| 65 |
+
p.add_argument("--image", help="path to an image to attach")
|
| 66 |
+
p.add_argument("--code", action="store_true", help="ask agents for a Python code solution")
|
| 67 |
+
args = p.parse_args()
|
| 68 |
+
uri = None
|
| 69 |
+
if args.image:
|
| 70 |
+
mime = mimetypes.guess_type(args.image)[0] or "image/png"
|
| 71 |
+
uri = f"data:{mime};base64," + base64.b64encode(open(args.image, "rb").read()).decode()
|
| 72 |
+
print(asyncio.run(run(" ".join(args.task), uri, args.code)))
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3fd169731d2cbde95e10bf356d66d5997fd885dd8dbb6fb4684da3f23b2585d8
|
| 3 |
+
size 11421892
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"bos_token": null,
|
| 5 |
+
"clean_up_tokenization_spaces": false,
|
| 6 |
+
"eos_token": "<|im_end|>",
|
| 7 |
+
"errors": "replace",
|
| 8 |
+
"extra_special_tokens": [
|
| 9 |
+
"<|im_start|>",
|
| 10 |
+
"<|im_end|>",
|
| 11 |
+
"<|object_ref_start|>",
|
| 12 |
+
"<|object_ref_end|>",
|
| 13 |
+
"<|box_start|>",
|
| 14 |
+
"<|box_end|>",
|
| 15 |
+
"<|quad_start|>",
|
| 16 |
+
"<|quad_end|>",
|
| 17 |
+
"<|vision_start|>",
|
| 18 |
+
"<|vision_end|>",
|
| 19 |
+
"<|vision_pad|>",
|
| 20 |
+
"<|image_pad|>",
|
| 21 |
+
"<|video_pad|>"
|
| 22 |
+
],
|
| 23 |
+
"is_local": false,
|
| 24 |
+
"local_files_only": false,
|
| 25 |
+
"model_max_length": 131072,
|
| 26 |
+
"pad_token": "<|endoftext|>",
|
| 27 |
+
"split_special_tokens": false,
|
| 28 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 29 |
+
"unk_token": null
|
| 30 |
+
}
|
train.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 3: full fine-tune of Qwen2.5-0.5B-Instruct into the Shaktimaan router (loss on the route only)."""
|
| 2 |
+
import os
|
| 3 |
+
from datasets import load_from_disk
|
| 4 |
+
from trl import SFTConfig, SFTTrainer
|
| 5 |
+
|
| 6 |
+
BASE = os.environ.get("BASE", "Qwen/Qwen2.5-0.5B-Instruct")
|
| 7 |
+
ds = load_from_disk("data/shaktimaan_routing").map(
|
| 8 |
+
lambda r: {"prompt": r["messages"][:2], "completion": r["messages"][2:]}).select_columns(["prompt", "completion"])
|
| 9 |
+
|
| 10 |
+
cfg = SFTConfig(
|
| 11 |
+
output_dir="ckpt", num_train_epochs=3, per_device_train_batch_size=8, gradient_accumulation_steps=4,
|
| 12 |
+
learning_rate=2e-5, lr_scheduler_type="cosine", warmup_steps=20, weight_decay=0.0, bf16=True,
|
| 13 |
+
max_length=3072, completion_only_loss=True, logging_steps=10, eval_strategy="epoch", save_strategy="epoch",
|
| 14 |
+
save_total_limit=1, report_to=[], gradient_checkpointing=True, dataloader_num_workers=4,
|
| 15 |
+
)
|
| 16 |
+
trainer = SFTTrainer(model=BASE, args=cfg, train_dataset=ds["train"], eval_dataset=ds["test"])
|
| 17 |
+
trainer.train()
|
| 18 |
+
trainer.save_model("shaktimaan-0.5b")
|
| 19 |
+
trainer.processing_class.save_pretrained("shaktimaan-0.5b")
|