Text Generation
Transformers
Safetensors
English
qwen2
verifier
answer-verification
llm-cascade
model-cascade
escalation
llm-router
orchestration
multi-agent
conversational
Eval Results (legacy)
text-generation-inference
Instructions to use podstack/Jedi with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use podstack/Jedi with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="podstack/Jedi") 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/Jedi") model = AutoModelForCausalLM.from_pretrained("podstack/Jedi", 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/Jedi with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "podstack/Jedi" # 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/Jedi", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/podstack/Jedi
- SGLang
How to use podstack/Jedi 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/Jedi" \ --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/Jedi", "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/Jedi" \ --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/Jedi", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use podstack/Jedi with Docker Model Runner:
docker model run hf.co/podstack/Jedi
Model card: diagnostics of what Jedi relies on
Browse files- README.md +59 -8
- diagnostics.py +95 -0
- diagnostics_ladder.py +79 -0
README.md
CHANGED
|
@@ -38,7 +38,12 @@ picks a model *before* any answer exists (see [How Jedi differs from Shaktimaan]
|
|
| 38 |
- Within 1.4 points of a *perfect* verifier (93.3%) on the same ladder.
|
| 39 |
- **AUROC 0.92** at telling correct answers from wrong ones on unseen tasks; calibrated confidence (ECE 0.017).
|
| 40 |
- One forward pass and **one output token** per check: ~56 ms at batch size 1, ~450 input tokens.
|
| 41 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
## How it works
|
| 44 |
|
|
@@ -76,6 +81,44 @@ picks a model *before* any answer exists (see [How Jedi differs from Shaktimaan]
|
|
| 76 |
4. **The single dial.** The threshold trades cost for accuracy. Raise it to escalate more and catch more wrong
|
| 77 |
answers; lower it to save money. The top rung is always accepted, so every task gets an answer.
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
## Quick start
|
| 80 |
|
| 81 |
### Score one answer
|
|
@@ -101,9 +144,12 @@ def p_correct(task, answer, agent="glm-5.3-flash", kind="num", finish="stop"):
|
|
| 101 |
return 1 / (1 + math.exp(-(cal["a"] * gap + cal["b"])))
|
| 102 |
|
| 103 |
task = "A shop sells pencils at 3 for $1.20. How much do 15 pencils cost, in dollars?"
|
| 104 |
-
print(p_correct(task, "Each pencil costs 1.20 / 3 = 0.40. 15 x 0.40 = 6.00.\nFinal answer: 6"))
|
| 105 |
-
print(p_correct(task, "
|
|
|
|
| 106 |
```
|
|
|
|
|
|
|
| 107 |
|
| 108 |
Always build the input with `common.judge_view(...)` and use `common.JEDI_SYSTEM` as the system prompt; Jedi was
|
| 109 |
trained on exactly that format. `kind` is the task type it was trained with: `num`, `math`, `mc`
|
|
@@ -151,9 +197,10 @@ problem, getting good answers without paying for the biggest model every time, a
|
|
| 151 |
|
| 152 |
**Why the switch.** Shaktimaan's main finding was that, on ordinary text and code tasks, *which* mid-size model
|
| 153 |
solves a given task is largely sampling noise that cannot be predicted from the prompt. A router has to guess
|
| 154 |
-
blind. Jedi waits until a cheap answer exists and
|
| 155 |
-
|
| 156 |
-
|
|
|
|
| 157 |
|
| 158 |
**Head to head**, on the same 491 held-out tasks with the same fresh agent samples:
|
| 159 |
|
|
@@ -261,8 +308,8 @@ Calibrated P(correct) on answers to tasks Jedi never saw. *False accept* = share
|
|
| 261 |
| ARC-Challenge | 300 | 4.0% | 0.842 | 91.7% |
|
| 262 |
| CommonsenseQA | 252 | 18.3% | 0.593 | 89.1% |
|
| 263 |
|
| 264 |
-
Jedi is strongest where
|
| 265 |
-
|
| 266 |
such as single-hop commonsense and easy science multiple choice.
|
| 267 |
|
| 268 |
### As the gate of the ladder
|
|
@@ -324,6 +371,9 @@ CommonsenseQA-heavy new-domain set, Jedi at 0.7 does not beat always using Qwen.
|
|
| 324 |
single forward pass and batch well.
|
| 325 |
|
| 326 |
## Limitations
|
|
|
|
|
|
|
|
|
|
| 327 |
- **Domain-bound.** Verification skill transfers poorly to unseen task families (BIG-Bench Hard AUROC 0.72) and is
|
| 328 |
near chance on CommonsenseQA (0.59). Measure it on your own traffic before relying on a threshold.
|
| 329 |
- **A third of wrong in-domain answers get through at 0.5.** False-accept rates are highest for the strongest
|
|
@@ -350,6 +400,7 @@ single forward pass and batch well.
|
|
| 350 |
| `train.py` | fine-tuning script (stage 3) |
|
| 351 |
| `evaluate.py` | verifier metrics, ladder, baselines and threshold sweep (stage 4) |
|
| 352 |
| `eval_results.json` | raw evaluation output behind every number on this card |
|
|
|
|
| 353 |
|
| 354 |
The graded answer data is not published; `gen.py` and `build.py` regenerate it given endpoints for the three models.
|
| 355 |
|
|
|
|
| 38 |
- Within 1.4 points of a *perfect* verifier (93.3%) on the same ladder.
|
| 39 |
- **AUROC 0.92** at telling correct answers from wrong ones on unseen tasks; calibrated confidence (ECE 0.017).
|
| 40 |
- One forward pass and **one output token** per check: ~56 ms at batch size 1, ~450 input tokens.
|
| 41 |
+
- **It judges how an answer looks, not whether its final value is right.** Swapping the final answer of a real
|
| 42 |
+
response barely moves its score. Much of its signal is surface cues (length, truncation, which model answered),
|
| 43 |
+
though it still beats a gate built only on those cues by 1-2 points on the ladder. See
|
| 44 |
+
[What Jedi actually picks up on](#what-jedi-actually-picks-up-on).
|
| 45 |
+
- **Weak outside its training domains**: AUROC 0.72 on BIG-Bench Hard (where answer length alone scores 0.80) and
|
| 46 |
+
0.59 on CommonsenseQA. See *Limitations*.
|
| 47 |
|
| 48 |
## How it works
|
| 49 |
|
|
|
|
| 81 |
4. **The single dial.** The threshold trades cost for accuracy. Raise it to escalate more and catch more wrong
|
| 82 |
answers; lower it to save money. The top rung is always accepted, so every task gets an answer.
|
| 83 |
|
| 84 |
+
### What Jedi actually picks up on
|
| 85 |
+
The system prompt asks Jedi to look for broken reasoning, bad arithmetic, failing code, hedging and truncation.
|
| 86 |
+
Training only rewards predicting the grade, though, so we tested what it learned (`diagnostics.py`,
|
| 87 |
+
`diagnostics_ladder.py`).
|
| 88 |
+
|
| 89 |
+
| check | in-domain test | unseen (BIG-Bench Hard) |
|
| 90 |
+
|---|---|---|
|
| 91 |
+
| Jedi AUROC | **0.919** | 0.721 |
|
| 92 |
+
| surface-feature baseline (logistic regression on answer length, truncated, empty final answer, agent, source) | 0.871 | **0.826** |
|
| 93 |
+
| answer length alone (shorter = more likely right) | 0.810 | 0.801 |
|
| 94 |
+
| Jedi, truncated answers excluded | 0.879 | 0.682 |
|
| 95 |
+
| Jedi ranks the right answer above a wrong one *to the same task* | 93.1% of 690 pairs | 84.6% of 1,168 pairs |
|
| 96 |
+
|
| 97 |
+
**Final-answer swap test.** We took 1,583 real test answers (multiple choice and numeric, not truncated) and changed
|
| 98 |
+
only the value after `Final answer:`. Right answers got a wrong value; wrong answers got the gold value.
|
| 99 |
+
|
| 100 |
+
| | mean P(correct) before | after swap |
|
| 101 |
+
|---|---|---|
|
| 102 |
+
| right answers -> wrong final value (1,443) | 0.912 | 0.902 |
|
| 103 |
+
| wrong answers -> gold final value (140) | 0.717 | 0.806 |
|
| 104 |
+
|
| 105 |
+
Jedi barely notices when the final answer contradicts the reasoning above it, or when a wrong answer is given the
|
| 106 |
+
correct final value. It scores the *body* of the response: its length, its shape, whether it ends cleanly, and the
|
| 107 |
+
style of reasoning that tends to accompany wrong answers from each model. Hand-written short answers with a planted
|
| 108 |
+
arithmetic slip are usually accepted.
|
| 109 |
+
|
| 110 |
+
**On the ladder, Jedi still beats the surface-feature gate** (same 491 tasks, best settings):
|
| 111 |
+
|
| 112 |
+
| gate | accuracy | relative cost |
|
| 113 |
+
|---|---|---|
|
| 114 |
+
| surface-feature baseline, threshold 0.05 | 89.8% | 0.224 |
|
| 115 |
+
| surface-feature baseline, threshold 0.1 | 89.8% | 0.395 |
|
| 116 |
+
| Jedi, threshold 0.05 | 91.0% | 0.244 |
|
| 117 |
+
| Jedi, threshold 0.7 | 91.9% | 0.454 |
|
| 118 |
+
|
| 119 |
+
So in its training domains, Jedi learned something real beyond length and truncation. That gain is 1-2 points of
|
| 120 |
+
end-to-end accuracy. On unseen task families, the surface features transfer better than Jedi does.
|
| 121 |
+
|
| 122 |
## Quick start
|
| 123 |
|
| 124 |
### Score one answer
|
|
|
|
| 144 |
return 1 / (1 + math.exp(-(cal["a"] * gap + cal["b"])))
|
| 145 |
|
| 146 |
task = "A shop sells pencils at 3 for $1.20. How much do 15 pencils cost, in dollars?"
|
| 147 |
+
print(p_correct(task, "Each pencil costs 1.20 / 3 = 0.40. 15 x 0.40 = 6.00.\nFinal answer: 6")) # 0.99 accept
|
| 148 |
+
print(p_correct(task, "Let me think step by step. First, 3 pencils cost 1.20, so", finish="length")) # 0.07 escalate
|
| 149 |
+
print(p_correct(task, "15 / 3 = 5 groups. 5 x 1.20 = 5.00.\nFinal answer: 5")) # 0.98 - missed
|
| 150 |
```
|
| 151 |
+
The third call shows the main limitation: a fluent answer with an arithmetic slip is accepted. Jedi is a learned
|
| 152 |
+
filter over real model outputs, not a checker of the math.
|
| 153 |
|
| 154 |
Always build the input with `common.judge_view(...)` and use `common.JEDI_SYSTEM` as the system prompt; Jedi was
|
| 155 |
trained on exactly that format. `kind` is the task type it was trained with: `num`, `math`, `mc`
|
|
|
|
| 197 |
|
| 198 |
**Why the switch.** Shaktimaan's main finding was that, on ordinary text and code tasks, *which* mid-size model
|
| 199 |
solves a given task is largely sampling noise that cannot be predicted from the prompt. A router has to guess
|
| 200 |
+
blind. Jedi waits until a cheap answer exists and looks at that particular sample. Signs that a sample went wrong
|
| 201 |
+
(rambling length, truncation, the reasoning style that comes with mistakes) only exist after it is written, so a
|
| 202 |
+
router can never see them. Jedi does not re-derive the answer, so fluent answers that are wrong still get through
|
| 203 |
+
(see [What Jedi actually picks up on](#what-jedi-actually-picks-up-on)).
|
| 204 |
|
| 205 |
**Head to head**, on the same 491 held-out tasks with the same fresh agent samples:
|
| 206 |
|
|
|
|
| 308 |
| ARC-Challenge | 300 | 4.0% | 0.842 | 91.7% |
|
| 309 |
| CommonsenseQA | 252 | 18.3% | 0.593 | 89.1% |
|
| 310 |
|
| 311 |
+
Jedi is strongest where wrong answers *look* different from right ones: multi-step math and code, where
|
| 312 |
+
failures tend to be long, meandering or cut off. It is weakest where a wrong answer reads exactly like a right one,
|
| 313 |
such as single-hop commonsense and easy science multiple choice.
|
| 314 |
|
| 315 |
### As the gate of the ladder
|
|
|
|
| 371 |
single forward pass and batch well.
|
| 372 |
|
| 373 |
## Limitations
|
| 374 |
+
- **Not a checker.** Jedi does not verify the final answer: changing it barely moves the score, and planted
|
| 375 |
+
arithmetic slips in otherwise fluent answers usually pass. It relies heavily on surface cues, and on unseen task
|
| 376 |
+
families a logistic regression on answer length, truncation and agent beats it (AUROC 0.83 vs 0.72).
|
| 377 |
- **Domain-bound.** Verification skill transfers poorly to unseen task families (BIG-Bench Hard AUROC 0.72) and is
|
| 378 |
near chance on CommonsenseQA (0.59). Measure it on your own traffic before relying on a threshold.
|
| 379 |
- **A third of wrong in-domain answers get through at 0.5.** False-accept rates are highest for the strongest
|
|
|
|
| 400 |
| `train.py` | fine-tuning script (stage 3) |
|
| 401 |
| `evaluate.py` | verifier metrics, ladder, baselines and threshold sweep (stage 4) |
|
| 402 |
| `eval_results.json` | raw evaluation output behind every number on this card |
|
| 403 |
+
| `diagnostics.py`, `diagnostics_ladder.py` | surface-feature baselines, answer-swap test, ladder with a surface-feature gate |
|
| 404 |
|
| 405 |
The graded answer data is not published; `gen.py` and `build.py` regenerate it given endpoints for the three models.
|
| 406 |
|
diagnostics.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Does Jedi read the answer content, or surface cues? (1) shallow-feature baselines (2) final-answer swap test."""
|
| 2 |
+
import json, math, random, re, sys
|
| 3 |
+
import numpy as np, torch
|
| 4 |
+
import common
|
| 5 |
+
from datasets import load_from_disk
|
| 6 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 7 |
+
|
| 8 |
+
def roc_auc_score(y, p):
|
| 9 |
+
y, p = np.asarray(y, bool), np.asarray(p, float)
|
| 10 |
+
r = np.argsort(np.argsort(p, kind="mergesort")) + 1
|
| 11 |
+
return float((r[y].sum() - y.sum() * (y.sum() + 1) / 2) / (y.sum() * (~y).sum()))
|
| 12 |
+
|
| 13 |
+
class LogisticRegression:
|
| 14 |
+
def fit(self, X, y):
|
| 15 |
+
X = torch.tensor(X, dtype=torch.float32); y = torch.tensor(np.asarray(y, float), dtype=torch.float32)
|
| 16 |
+
self.mu, self.sd = X.mean(0), X.std(0) + 1e-6
|
| 17 |
+
X = (X - self.mu) / self.sd
|
| 18 |
+
self.w = torch.zeros(X.shape[1], requires_grad=True); self.b = torch.zeros(1, requires_grad=True)
|
| 19 |
+
opt = torch.optim.LBFGS([self.w, self.b], max_iter=500)
|
| 20 |
+
def closure():
|
| 21 |
+
opt.zero_grad(); l = torch.nn.functional.binary_cross_entropy_with_logits(X @ self.w + self.b, y); l.backward(); return l
|
| 22 |
+
opt.step(closure); return self
|
| 23 |
+
def predict_proba(self, X):
|
| 24 |
+
X = (torch.tensor(X, dtype=torch.float32) - self.mu) / self.sd
|
| 25 |
+
p = torch.sigmoid(X @ self.w + self.b).detach().numpy(); return np.stack([1 - p, p], 1)
|
| 26 |
+
random.seed(0)
|
| 27 |
+
P = sys.argv[1] if len(sys.argv) > 1 else "jedi-0.5b"
|
| 28 |
+
cal = json.load(open(P + "/calibration.json")); tok = AutoTokenizer.from_pretrained(P); tok.padding_side = "left"
|
| 29 |
+
model = AutoModelForCausalLM.from_pretrained(P, dtype=torch.bfloat16).cuda().eval()
|
| 30 |
+
ds = load_from_disk("data/jedi_judge")
|
| 31 |
+
|
| 32 |
+
def score(views, bs=16):
|
| 33 |
+
out = np.zeros(len(views)); order = np.argsort([len(v) for v in views])
|
| 34 |
+
for i in range(0, len(views), bs):
|
| 35 |
+
idx = order[i:i + bs]
|
| 36 |
+
msgs = [[{"role": "system", "content": common.JEDI_SYSTEM}, {"role": "user", "content": views[j]}] for j in idx]
|
| 37 |
+
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", padding=True, return_dict=True).to("cuda")
|
| 38 |
+
with torch.no_grad(): l = model(**enc).logits[:, -1].float()
|
| 39 |
+
g = (l[:, cal["accept_token"]] - l[:, cal["escalate_token"]]).cpu().numpy()
|
| 40 |
+
out[idx] = 1 / (1 + np.exp(-(cal["a"] * g + cal["b"])))
|
| 41 |
+
return out
|
| 42 |
+
|
| 43 |
+
def feats(d):
|
| 44 |
+
ag = sorted(set(ds["train"]["agent"])); src = sorted(set(ds["train"]["source"]) | set(ds["ood"]["source"]))
|
| 45 |
+
X = []
|
| 46 |
+
for r in d:
|
| 47 |
+
X.append([math.log1p(r["completion_tokens"]), r["finish"] == "length", not r["pred"].strip()]
|
| 48 |
+
+ [r["agent"] == a for a in ag] + [r["source"] == s for s in src])
|
| 49 |
+
return np.array(X, float)
|
| 50 |
+
|
| 51 |
+
base = ds["train"].filter(lambda r: r["sample"] == 0) # one sample per (task, agent): no oversampling dupes in fit
|
| 52 |
+
lr = LogisticRegression().fit(feats(base), base["correct"])
|
| 53 |
+
res = {}
|
| 54 |
+
for sp in ("test", "ood"):
|
| 55 |
+
d = ds[sp]; y = np.array(d["correct"])
|
| 56 |
+
p = score(d["judge_input"])
|
| 57 |
+
trunc = np.array([f == "length" for f in d["finish"]])
|
| 58 |
+
res[sp] = dict(
|
| 59 |
+
jedi=roc_auc_score(y, p),
|
| 60 |
+
shallow_logreg=roc_auc_score(y, lr.predict_proba(feats(d))[:, 1]),
|
| 61 |
+
length_only=roc_auc_score(y, -np.array(d["completion_tokens"])),
|
| 62 |
+
jedi_not_truncated=roc_auc_score(y[~trunc], p[~trunc]),
|
| 63 |
+
truncated_share=float(trunc.mean()))
|
| 64 |
+
# within-task: tasks with both a right and a wrong answer -> does Jedi rank the right one higher?
|
| 65 |
+
by = {}
|
| 66 |
+
for i, r in enumerate(d):
|
| 67 |
+
by.setdefault(r["task_id"], []).append(i)
|
| 68 |
+
wins = tot = 0
|
| 69 |
+
for ids in by.values():
|
| 70 |
+
for i in ids:
|
| 71 |
+
for j in ids:
|
| 72 |
+
if y[i] and not y[j]:
|
| 73 |
+
tot += 1; wins += (p[i] > p[j]) + 0.5 * (p[i] == p[j])
|
| 74 |
+
res[sp]["within_task_pairwise_acc"] = wins / tot; res[sp]["within_task_pairs"] = tot
|
| 75 |
+
if sp == "test":
|
| 76 |
+
# swap test: replace the 'Final answer:' value. wrong -> gold, right -> a wrong value.
|
| 77 |
+
orig, swapped, ys = [], [], []
|
| 78 |
+
for r in d:
|
| 79 |
+
if r["kind"] not in ("mc", "num") or r["finish"] == "length": continue
|
| 80 |
+
m = list(re.finditer(r"(Final answer\s*[::]\s*)(.+)", r["judge_input"], flags=re.I))
|
| 81 |
+
if not m: continue
|
| 82 |
+
m = m[-1]; gold = r["gold"].strip()
|
| 83 |
+
if r["correct"]:
|
| 84 |
+
new = random.choice([c for c in "ABCDEFGHIJ"[:5] if c != gold.upper()]) if r["kind"] == "mc" else str(int(float(gold.replace(',', ''))) + 7) if re.fullmatch(r"-?[\d,]+(\.0+)?", gold) else None
|
| 85 |
+
else:
|
| 86 |
+
new = gold
|
| 87 |
+
if new is None: continue
|
| 88 |
+
v = r["judge_input"][:m.start(2)] + new + r["judge_input"][m.end(2):]
|
| 89 |
+
orig.append(r["judge_input"]); swapped.append(v); ys.append(r["correct"])
|
| 90 |
+
po, ps = score(orig), score(swapped); ys = np.array(ys)
|
| 91 |
+
res["swap"] = dict(n=len(ys), n_right_made_wrong=int(ys.sum()), n_wrong_made_right=int((~ys).sum()),
|
| 92 |
+
right_orig=float(po[ys].mean()), right_after_swap_to_wrong=float(ps[ys].mean()),
|
| 93 |
+
wrong_orig=float(po[~ys].mean()), wrong_after_swap_to_gold=float(ps[~ys].mean()),
|
| 94 |
+
auroc_on_swapped=roc_auc_score(~ys, ps)) # label of swapped = flipped correctness
|
| 95 |
+
print(json.dumps(res, indent=1))
|
diagnostics_ladder.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ladder (glm -> qwen -> nemotron-ultra) on the 491 held-out tasks, gated by Jedi vs by the surface-feature baseline."""
|
| 2 |
+
import json, math, random, re, sys
|
| 3 |
+
import numpy as np, torch
|
| 4 |
+
import common
|
| 5 |
+
from datasets import load_from_disk
|
| 6 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 7 |
+
|
| 8 |
+
def roc_auc_score(y, p):
|
| 9 |
+
y, p = np.asarray(y, bool), np.asarray(p, float)
|
| 10 |
+
r = np.argsort(np.argsort(p, kind="mergesort")) + 1
|
| 11 |
+
return float((r[y].sum() - y.sum() * (y.sum() + 1) / 2) / (y.sum() * (~y).sum()))
|
| 12 |
+
|
| 13 |
+
class LogisticRegression:
|
| 14 |
+
def fit(self, X, y):
|
| 15 |
+
X = torch.tensor(X, dtype=torch.float32); y = torch.tensor(np.asarray(y, float), dtype=torch.float32)
|
| 16 |
+
self.mu, self.sd = X.mean(0), X.std(0) + 1e-6
|
| 17 |
+
X = (X - self.mu) / self.sd
|
| 18 |
+
self.w = torch.zeros(X.shape[1], requires_grad=True); self.b = torch.zeros(1, requires_grad=True)
|
| 19 |
+
opt = torch.optim.LBFGS([self.w, self.b], max_iter=500)
|
| 20 |
+
def closure():
|
| 21 |
+
opt.zero_grad(); l = torch.nn.functional.binary_cross_entropy_with_logits(X @ self.w + self.b, y); l.backward(); return l
|
| 22 |
+
opt.step(closure); return self
|
| 23 |
+
def predict_proba(self, X):
|
| 24 |
+
X = (torch.tensor(X, dtype=torch.float32) - self.mu) / self.sd
|
| 25 |
+
p = torch.sigmoid(X @ self.w + self.b).detach().numpy(); return np.stack([1 - p, p], 1)
|
| 26 |
+
random.seed(0)
|
| 27 |
+
P = sys.argv[1] if len(sys.argv) > 1 else "jedi-0.5b"
|
| 28 |
+
cal = json.load(open(P + "/calibration.json")); tok = AutoTokenizer.from_pretrained(P); tok.padding_side = "left"
|
| 29 |
+
model = AutoModelForCausalLM.from_pretrained(P, dtype=torch.bfloat16).cuda().eval()
|
| 30 |
+
ds = load_from_disk("data/jedi_judge")
|
| 31 |
+
|
| 32 |
+
def score(views, bs=16):
|
| 33 |
+
out = np.zeros(len(views)); order = np.argsort([len(v) for v in views])
|
| 34 |
+
for i in range(0, len(views), bs):
|
| 35 |
+
idx = order[i:i + bs]
|
| 36 |
+
msgs = [[{"role": "system", "content": common.JEDI_SYSTEM}, {"role": "user", "content": views[j]}] for j in idx]
|
| 37 |
+
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", padding=True, return_dict=True).to("cuda")
|
| 38 |
+
with torch.no_grad(): l = model(**enc).logits[:, -1].float()
|
| 39 |
+
g = (l[:, cal["accept_token"]] - l[:, cal["escalate_token"]]).cpu().numpy()
|
| 40 |
+
out[idx] = 1 / (1 + np.exp(-(cal["a"] * g + cal["b"])))
|
| 41 |
+
return out
|
| 42 |
+
|
| 43 |
+
def feats(d):
|
| 44 |
+
ag = sorted(set(ds["train"]["agent"])); src = sorted(set(ds["train"]["source"]) | set(ds["ood"]["source"]))
|
| 45 |
+
X = []
|
| 46 |
+
for r in d:
|
| 47 |
+
X.append([math.log1p(r["completion_tokens"]), r["finish"] == "length", not r["pred"].strip()]
|
| 48 |
+
+ [r["agent"] == a for a in ag] + [r["source"] == s for s in src])
|
| 49 |
+
return np.array(X, float)
|
| 50 |
+
|
| 51 |
+
base = ds["train"].filter(lambda r: r["sample"] == 0) # one sample per (task, agent): no oversampling dupes in fit
|
| 52 |
+
lr = LogisticRegression().fit(feats(base), base["correct"])
|
| 53 |
+
|
| 54 |
+
from datasets import load_dataset
|
| 55 |
+
from common import LADDER, POOL, STRONGEST
|
| 56 |
+
CHEAP, MID = LADDER[0], LADDER[1]
|
| 57 |
+
d = ds["test"]
|
| 58 |
+
pj = score(d["judge_input"]); ps = lr.predict_proba(feats(d))[:, 1]
|
| 59 |
+
R = {}
|
| 60 |
+
for r, a, b in zip(d.select_columns(["task_id", "agent", "sample", "correct", "completion_tokens"]), pj, ps):
|
| 61 |
+
R[r["task_id"], r["agent"], r["sample"]] = dict(r, pj=float(a), ps=float(b))
|
| 62 |
+
shak = {r["id"]: r for sp in ("train", "test") for r in load_dataset("podstack/Shaktimaan-routing", split=sp)}
|
| 63 |
+
srcs = {"gsm8k", "math", "mmlupro", "arc", "mbpp", "humaneval"}
|
| 64 |
+
tasks = sorted({t for (t, _, _) in R if t.split("-")[0] in srcs and (t, CHEAP, 0) in R and (t, MID, 0) in R and t in shak})
|
| 65 |
+
def ultra(t):
|
| 66 |
+
x = json.loads(shak[t]["agent_results"])[STRONGEST]; return x["correct"], x["completion_tokens"]
|
| 67 |
+
ucost = sum(POOL[STRONGEST]["cost"] * ultra(t)[1] for t in tasks)
|
| 68 |
+
def ladder(key, tau):
|
| 69 |
+
c = cost = 0
|
| 70 |
+
for t in tasks:
|
| 71 |
+
for i, ag in enumerate(LADDER):
|
| 72 |
+
if i == 2:
|
| 73 |
+
ok, tk = ultra(t); cost += POOL[ag]["cost"] * tk; c += ok; break
|
| 74 |
+
a = R[t, ag, 0]; cost += POOL[ag]["cost"] * a["completion_tokens"]
|
| 75 |
+
if a[key] >= tau: c += a["correct"]; break
|
| 76 |
+
return round(c / len(tasks), 4), round(cost / ucost, 4)
|
| 77 |
+
print("n", len(tasks))
|
| 78 |
+
for tau in (0.05, 0.1, 0.2, 0.3, 0.4, 0.45, 0.5):
|
| 79 |
+
print(tau, "jedi", ladder("pj", tau), "shallow", ladder("ps", tau))
|