CommanderCuth commited on
Commit
cd34530
·
verified ·
1 Parent(s): 74ae095

Upload 11 files

Browse files
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ *.jsonl
5
+ !.gitignore
6
+ !eval/problems_easy.jsonl
7
+ !eval/problems_hard.jsonl
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacob Thomas Cuthbertson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - metacog
5
+ - ensemble
6
+ - system-one
7
+ - text-generation
8
+ base_model:
9
+ - prism-ml/Ternary-Bonsai-2-27B-gguf
10
+ - Mapika/decider-4b
11
+ ---
12
+
13
+ # Heuristic 1
14
+
15
+ One model out of two: **Bonsai 2 27B (ternary)** thinks, **Decider-4B** judges,
16
+ [MetaCog](https://github.com/ItIsCuthNotCup/MetaCog) wires them into a single
17
+ OpenAI-compatible endpoint.
18
+
19
+ > **This is not a weight merge.** No tensors were averaged. It is an
20
+ > inference-time pipeline merge: the thinker writes candidate answers, an
21
+ > independent System-One judge (Decider-4B) scores them, and the winner is
22
+ > returned. One endpoint, one model ID, two models inside.
23
+
24
+ ```
25
+ user prompt ──▶ bonsai-27B ──▶ greedy anchor judged FIRST
26
+ │ ≥ 0.80 and finished? ──▶ answer immediately
27
+ ▼
28
+ 7 sampled paths IN PARALLEL,
29
+ each judged the moment it finishes;
30
+ first finished path ≥ 0.80 wins on the spot,
31
+ the rest are abandoned
32
+ │
33
+ (nothing clears the bar? best judged
34
+ finished path wins)
35
+ ```
36
+
37
+ The judge is *not* the thinker — that separation is the whole point.
38
+
39
+ `race.py` implements this loop (parallel generation, judge-as-they-finish,
40
+ early stop). The old sequential MetaCog `best_of_n` is still available via
41
+ `NO_RACE=1` (server) or `--no-race` (batch) for A/B comparison. The
42
+ early-stop bar is `STOP_CONFIDENCE` / `--stop-confidence` (default 0.80).
43
+
44
+ > **On the 0.80 bar, honestly:** the MetaCog repo measured its 0.95 cascade
45
+ > threshold on real Jev (a noul ≥ 0.95 was correct 226/229 times). 0.80 did
46
+ > not win anywhere — it is the operator's directive and is untested on
47
+ > decider-4b, whose confidence scale is not calibrated (values above 1.0
48
+ > have been observed). Measure before trusting it.
49
+
50
+ ## Measured
51
+
52
+ | eval | bonsai alone | merge (Heuristic 1) | note |
53
+ |---|---|---|---|
54
+ | HumanEval, n=164, true pass@1 (prompt+completion graded as one program, `<think>` stripped) | 108/164 (65.9%) | **135/164 (82.3%)** | judge fixed 27, broke 0; McNemar p < 1e-6 |
55
+ | 12 easy Q&A (verified) | 12/12 | 12/12 | tie; nothing to fix |
56
+ | 10 hard Q&A (verified, tricks + big arithmetic) | 8/10 | **10/10** | judge fixed 2, broke 0 |
57
+
58
+ Cost per problem (HumanEval averages): merge uses ~5,600 thinker tokens
59
+ over 2.0 thinker calls + 8.5 judge calls; bonsai alone uses ~700 tokens in
60
+ 1 call. Roughly 8× the thinker tokens for +16 points of accuracy, $0 on
61
+ local hardware.
62
+
63
+ Latency: when the judge is confident in the greedy answer it returns
64
+ immediately (~same as bonsai alone — e.g. 2.5s on a trivial prompt); the
65
+ full vote only runs when uncertain. Measured on 3 HumanEval problems where
66
+ the full vote ran (DGX Spark, sequential paths, old loop): heuristic-1 took
67
+ 210s / 406s / 440s vs bonsai alone at 17s / 18s / 18s — roughly 12–24× slower
68
+ wall-clock on hard problems. The race loop (`race.py`) parallelizes the
69
+ sampled paths and cuts off the moment any finished path clears 0.80, which
70
+ removes most of that gap; re-measure on the Spark to confirm. Every response
71
+ carries `merge.judge_confidence`.
72
+
73
+ ## Run it
74
+
75
+ ```bash
76
+ pip install -r requirements.txt # metacog + httpx + pydantic
77
+
78
+ # one model on :8200
79
+ python server.py
80
+ # POST /v1/chat/completions {"messages": [...], "model": "bonsai-decider4b-merge"}
81
+
82
+ # batch runner
83
+ python merge.py --problem "What is 17*23?"
84
+ python merge.py --problems problems.jsonl --out results.jsonl
85
+ ```
86
+
87
+ Env knobs: `THINKER_URL` (default `http://localhost:8010`),
88
+ `THINKER_MODEL` (default `bonsai`), `JUDGE_URL` (default
89
+ `http://localhost:8008`), `PORT` (8200), `N_PATHS` (8), `NO_CASCADE`,
90
+ `MAX_TOKENS` (2048), `TEMPERATURE` (0.8).
91
+
92
+ You need both backends up:
93
+
94
+ - **Thinker** — any OpenAI-compatible chat endpoint serving Bonsai 2 27B.
95
+ Reference setup: bjev's PrismML llama.cpp fork on `:8010`
96
+ (`Ternary-Bonsai-2-27B-PQ2_0.gguf`, 6.66 GiB).
97
+ - **Judge** — a `/v1/systemone` endpoint serving Decider-4B.
98
+ Reference: `runs/judge_server.py` (a `/v1/systemone` shim over the
99
+ `Mapika/decider-4b` checkpoint) on `:8008`.
100
+
101
+ On a DGX Spark this fits alongside both models in ~20 GB and runs at $0.
102
+
103
+ ## Files
104
+
105
+ - `server.py` — the one-model OpenAI-compatible server (`:8200`)
106
+ - `merge.py` — batch runner (problems in, answers + confidence + timings out)
107
+ - `eval/ab_bonsai_vs_merge.py` — A/B harness: bonsai alone vs the merge
108
+ - `eval/problems_easy.jsonl`, `eval/problems_hard.jsonl` — the measured sets
109
+
110
+ ## License
111
+
112
+ MIT. This repo ships pipeline code only — no weights. Parent model licenses
113
+ (checked 2026-09-27):
114
+ - Bonsai 2 27B ternary (`prism-ml/Ternary-Bonsai-2-27B-gguf`) — Apache-2.0
115
+ - Decider-4B (`Mapika/decider-4b`) — Apache-2.0
116
+ - MetaCog — MIT; llama.cpp — MIT
117
+ Fetch the weights yourself under their Apache-2.0 terms; nothing here
118
+ relicenses them. Not affiliated with PrismML, Mapika, or Alibaba.
eval/ab2_bonsai_vs_merge.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """A/B: bonsai alone (greedy, temp 0) vs the merge (bonsai + decider-4b judge).
3
+
4
+ Both served on the Spark: bonsai :8010, merge :8200.
5
+ Usage: .venv/bin/python runs/ab_bonsai_vs_merge.py
6
+ """
7
+ import json
8
+ import re
9
+ import time
10
+ import urllib.request
11
+
12
+ PROBLEMS = [
13
+ ("What is 1234 * 5678? Reply with just the number.", "7006652"),
14
+ ("A bat and ball cost $1.10. The bat costs $1 more than the ball. How much does the ball cost in cents? Reply with just the number.", "5"),
15
+ ("What is the 16th letter of the alphabet? Reply with just the letter.", "p"),
16
+ ("If you have 3 apples and take away 2, how many do you have? Reply with just the number.", "2"),
17
+ ("What year did the Berlin Wall fall? Reply with just the year.", "1989"),
18
+ ("How many sides does a heptagon have? Reply with just the number.", "7"),
19
+ ("What is 1/3 + 1/6? Reply as a fraction in lowest terms.", "1/2"),
20
+ ("Which planet has the most moons as of 2024, Saturn or Jupiter? Reply with just the name.", "saturn"),
21
+ ("A farmer has 17 sheep, all but 9 die. How many are left? Reply with just the number.", "9"),
22
+ ("What is the square root of 144? Reply with just the number.", "12"),
23
+ ]
24
+
25
+
26
+ def chat(url, model, problem, temperature):
27
+ body = json.dumps({
28
+ "model": model,
29
+ "messages": [{"role": "user", "content": problem}],
30
+ "max_tokens": 256,
31
+ "temperature": temperature,
32
+ }).encode()
33
+ req = urllib.request.Request(url, data=body,
34
+ headers={"Content-Type": "application/json"})
35
+ with urllib.request.urlopen(req, timeout=300) as r:
36
+ return json.load(r)
37
+
38
+
39
+ def final_text(answer):
40
+ # strip <think>...</think>, take the last non-empty line
41
+ t = re.sub(r"<think>.*?</think>", "", answer, flags=re.S | re.I).strip()
42
+ lines = [l.strip() for l in t.splitlines() if l.strip()]
43
+ return lines[-1] if lines else t
44
+
45
+
46
+ def correct(final, expect):
47
+ return expect.lower() in final.lower()
48
+
49
+
50
+ def main():
51
+ rows = []
52
+ for i, (problem, expect) in enumerate(PROBLEMS):
53
+ # bonsai alone
54
+ t0 = time.time()
55
+ rb = chat("http://localhost:8010/v1/chat/completions", "bonsai",
56
+ problem, 0.0)
57
+ tb = time.time() - t0
58
+ ab = rb["choices"][0]["message"]["content"]
59
+ fb, cb = final_text(ab), correct(final_text(ab), expect)
60
+
61
+ # merge
62
+ t0 = time.time()
63
+ rm = chat("http://localhost:8200/v1/chat/completions",
64
+ "heuristic-1", problem, 0.0)
65
+ tm = time.time() - t0
66
+ am = rm["choices"][0]["message"]["content"]
67
+ fm, cm = final_text(am), correct(final_text(am), expect)
68
+ conf = rm.get("merge", {}).get("judge_confidence", 0)
69
+
70
+ rows.append({"i": i, "problem": problem, "expect": expect,
71
+ "bonsai": fb[:120], "bonsai_ok": cb, "bonsai_s": round(tb, 1),
72
+ "merge": fm[:120], "merge_ok": cm, "merge_s": round(tm, 1),
73
+ "conf": conf})
74
+ print(f"[{i+1}/{len(PROBLEMS)}] expect={expect!r} "
75
+ f"bonsai={'OK ' if cb else 'MISS'} ({tb:.0f}s) "
76
+ f"merge={'OK ' if cm else 'MISS'} ({tm:.0f}s, conf={conf:.2f})",
77
+ flush=True)
78
+
79
+ b_ok = sum(r["bonsai_ok"] for r in rows)
80
+ m_ok = sum(r["merge_ok"] for r in rows)
81
+ flips = [r for r in rows if r["merge_ok"] and not r["bonsai_ok"]]
82
+ losses = [r for r in rows if r["bonsai_ok"] and not r["merge_ok"]]
83
+ print(f"\n=== bonsai alone: {b_ok}/{len(rows)} | merge: {m_ok}/{len(rows)} ===")
84
+ print(f"judge fixed: {len(flips)} | judge broke: {len(losses)}")
85
+ for r in flips:
86
+ print(f" FIXED: {r['problem'][:60]}... bonsai={r['bonsai'][:40]!r} merge={r['merge'][:40]!r}")
87
+ for r in losses:
88
+ print(f" BROKE: {r['problem'][:60]}... bonsai={r['bonsai'][:40]!r} merge={r['merge'][:40]!r}")
89
+ json.dump(rows, open("runs/ab2_results.json", "w"), indent=1)
90
+ print("wrote runs/ab2_results.json")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
eval/ab_bonsai_vs_merge.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """A/B: bonsai alone (greedy, temp 0) vs the merge (bonsai + decider-4b judge).
3
+
4
+ Both served on the Spark: bonsai :8010, merge :8200.
5
+ Usage: .venv/bin/python runs/ab_bonsai_vs_merge.py
6
+ """
7
+ import json
8
+ import re
9
+ import time
10
+ import urllib.request
11
+
12
+ PROBLEMS = [
13
+ ("What is 17*23? Reply with just the number.", "391"),
14
+ ("A train travels 120 km in 1.5 hours. What is its average speed in km/h? Reply with just the number.", "80"),
15
+ ("What is the 5th prime number? Reply with just the number.", "11"),
16
+ ("If all bloops are razzies and all razzies are lazzies, are all bloops lazzies? Reply yes or no.", "yes"),
17
+ ("Name the planet closest to the sun. Reply with just the name.", "mercury"),
18
+ ("What is 15% of 200? Reply with just the number.", "30"),
19
+ ("A rectangle has length 9 and width 4. What is its area? Reply with just the number.", "36"),
20
+ ("Which is larger: 3/4 or 5/7? Reply with just the fraction.", "3/4"),
21
+ ("What is the capital of Australia? Reply with just the city name.", "canberra"),
22
+ ("If you flip a fair coin twice, what is the probability both are heads? Reply as a fraction.", "1/4"),
23
+ ("What is 7^3? Reply with just the number.", "343"),
24
+ ("A book costs $12 after a 20% discount. What was the original price in dollars? Reply with just the number.", "15"),
25
+ ]
26
+
27
+
28
+ def chat(url, model, problem, temperature):
29
+ body = json.dumps({
30
+ "model": model,
31
+ "messages": [{"role": "user", "content": problem}],
32
+ "max_tokens": 256,
33
+ "temperature": temperature,
34
+ }).encode()
35
+ req = urllib.request.Request(url, data=body,
36
+ headers={"Content-Type": "application/json"})
37
+ with urllib.request.urlopen(req, timeout=300) as r:
38
+ return json.load(r)
39
+
40
+
41
+ def final_text(answer):
42
+ # strip <think>...</think>, take the last non-empty line
43
+ t = re.sub(r"<think>.*?</think>", "", answer, flags=re.S | re.I).strip()
44
+ lines = [l.strip() for l in t.splitlines() if l.strip()]
45
+ return lines[-1] if lines else t
46
+
47
+
48
+ def correct(final, expect):
49
+ return expect.lower() in final.lower()
50
+
51
+
52
+ def main():
53
+ rows = []
54
+ for i, (problem, expect) in enumerate(PROBLEMS):
55
+ # bonsai alone
56
+ t0 = time.time()
57
+ rb = chat("http://localhost:8010/v1/chat/completions", "bonsai",
58
+ problem, 0.0)
59
+ tb = time.time() - t0
60
+ ab = rb["choices"][0]["message"]["content"]
61
+ fb, cb = final_text(ab), correct(final_text(ab), expect)
62
+
63
+ # merge
64
+ t0 = time.time()
65
+ rm = chat("http://localhost:8200/v1/chat/completions",
66
+ "heuristic-1", problem, 0.0)
67
+ tm = time.time() - t0
68
+ am = rm["choices"][0]["message"]["content"]
69
+ fm, cm = final_text(am), correct(final_text(am), expect)
70
+ conf = rm.get("merge", {}).get("judge_confidence", 0)
71
+
72
+ rows.append({"i": i, "problem": problem, "expect": expect,
73
+ "bonsai": fb[:120], "bonsai_ok": cb, "bonsai_s": round(tb, 1),
74
+ "merge": fm[:120], "merge_ok": cm, "merge_s": round(tm, 1),
75
+ "conf": conf})
76
+ print(f"[{i+1}/{len(PROBLEMS)}] expect={expect!r} "
77
+ f"bonsai={'OK ' if cb else 'MISS'} ({tb:.0f}s) "
78
+ f"merge={'OK ' if cm else 'MISS'} ({tm:.0f}s, conf={conf:.2f})",
79
+ flush=True)
80
+
81
+ b_ok = sum(r["bonsai_ok"] for r in rows)
82
+ m_ok = sum(r["merge_ok"] for r in rows)
83
+ flips = [r for r in rows if r["merge_ok"] and not r["bonsai_ok"]]
84
+ losses = [r for r in rows if r["bonsai_ok"] and not r["merge_ok"]]
85
+ print(f"\n=== bonsai alone: {b_ok}/{len(rows)} | merge: {m_ok}/{len(rows)} ===")
86
+ print(f"judge fixed: {len(flips)} | judge broke: {len(losses)}")
87
+ for r in flips:
88
+ print(f" FIXED: {r['problem'][:60]}... bonsai={r['bonsai'][:40]!r} merge={r['merge'][:40]!r}")
89
+ for r in losses:
90
+ print(f" BROKE: {r['problem'][:60]}... bonsai={r['bonsai'][:40]!r} merge={r['merge'][:40]!r}")
91
+ json.dump(rows, open("runs/ab_results.json", "w"), indent=1)
92
+ print("wrote runs/ab_results.json")
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()
eval/problems_easy.jsonl ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"problem": "What is 17*23? Reply with just the number.", "expect": "391"}
2
+ {"problem": "A train travels 120 km in 1.5 hours. What is its average speed in km/h? Reply with just the number.", "expect": "80"}
3
+ {"problem": "What is the 5th prime number? Reply with just the number.", "expect": "11"}
4
+ {"problem": "If all bloops are razzies and all razzies are lazzies, are all bloops lazzies? Reply yes or no.", "expect": "yes"}
5
+ {"problem": "Name the planet closest to the sun. Reply with just the name.", "expect": "mercury"}
6
+ {"problem": "What is 15% of 200? Reply with just the number.", "expect": "30"}
7
+ {"problem": "A rectangle has length 9 and width 4. What is its area? Reply with just the number.", "expect": "36"}
8
+ {"problem": "Which is larger: 3/4 or 5/7? Reply with just the fraction.", "expect": "3/4"}
9
+ {"problem": "What is the capital of Australia? Reply with just the city name.", "expect": "canberra"}
10
+ {"problem": "If you flip a fair coin twice, what is the probability both are heads? Reply as a fraction.", "expect": "1/4"}
11
+ {"problem": "What is 7^3? Reply with just the number.", "expect": "343"}
12
+ {"problem": "A book costs $12 after a 20% discount. What was the original price in dollars? Reply with just the number.", "expect": "15"}
eval/problems_hard.jsonl ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {"problem": "What is 1234 * 5678? Reply with just the number.", "expect": "7006652"}
2
+ {"problem": "A bat and ball cost $1.10. The bat costs $1 more than the ball. How much does the ball cost in cents? Reply with just the number.", "expect": "5"}
3
+ {"problem": "What is the 16th letter of the alphabet? Reply with just the letter.", "expect": "p"}
4
+ {"problem": "If you have 3 apples and take away 2, how many do you have? Reply with just the number.", "expect": "2"}
5
+ {"problem": "What year did the Berlin Wall fall? Reply with just the year.", "expect": "1989"}
6
+ {"problem": "How many sides does a heptagon have? Reply with just the number.", "expect": "7"}
7
+ {"problem": "What is 1/3 + 1/6? Reply as a fraction in lowest terms.", "expect": "1/2"}
8
+ {"problem": "Which planet has the most moons as of 2024, Saturn or Jupiter? Reply with just the name.", "expect": "saturn"}
9
+ {"problem": "A farmer has 17 sheep, all but 9 die. How many are left? Reply with just the number.", "expect": "9"}
10
+ {"problem": "What is the square root of 144? Reply with just the number.", "expect": "12"}
merge.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """The merge: bonsai thinks, decider-4b judges, MetaCog wires them into one model.
3
+
4
+ thinker: bonsai (bjev llama.cpp, OpenAI chat) .... http://localhost:8010
5
+ judge: decider-4b (/v1/systemone shim) ......... http://localhost:8008
6
+
7
+ Usage:
8
+ .venv/bin/python merge.py --problem "What is 17*23?"
9
+ .venv/bin/python merge.py --problems problems.jsonl --out results.jsonl
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ import time
18
+
19
+ import re
20
+
21
+ def clean_answer(text: str) -> str:
22
+ """Strip <think>...</think> reasoning blocks the thinker may leak."""
23
+ return re.sub(r"<think>.*?</think>\s*", "", text, flags=re.S).strip()
24
+
25
+
26
+ from metacog import Config, MetaCog, OpenAICompatThinker, SystemOneJudge
27
+
28
+ import os
29
+
30
+ THINKER_URL = os.environ.get("THINKER_URL", "http://localhost:8010")
31
+ THINKER_MODEL = os.environ.get("THINKER_MODEL", "bonsai")
32
+ JUDGE_URL = os.environ.get("JUDGE_URL", "http://localhost:8008")
33
+
34
+
35
+ def build_thinker_judge():
36
+ thinker = OpenAICompatThinker(THINKER_URL, model=THINKER_MODEL)
37
+ # non-TypeSafe base_url -> no auth header; the local shim is open
38
+ judge = SystemOneJudge(base_url=JUDGE_URL)
39
+ return thinker, judge
40
+
41
+
42
+ def build(n_paths: int = 8, no_cascade: bool = False) -> MetaCog:
43
+ thinker, judge = build_thinker_judge()
44
+ config = Config(
45
+ mode="best_of_n",
46
+ strategy="noul",
47
+ n_paths=n_paths,
48
+ greedy_anchor=True,
49
+ cascade_confidence=None if no_cascade else 0.95,
50
+ max_tokens=2048,
51
+ temperature=0.8,
52
+ )
53
+ return MetaCog(thinker, judge, config)
54
+
55
+
56
+ def main() -> int:
57
+ ap = argparse.ArgumentParser(description="bonsai + decider-4b MetaCog merge")
58
+ src = ap.add_mutually_exclusive_group(required=True)
59
+ src.add_argument("--problem")
60
+ src.add_argument("--problems")
61
+ ap.add_argument("--n-paths", type=int, default=8)
62
+ ap.add_argument("--no-cascade", action="store_true")
63
+ ap.add_argument("--stop-confidence", type=float, default=0.80,
64
+ help="race loop early-stop bar (default 0.80)")
65
+ ap.add_argument("--no-race", action="store_true",
66
+ help="use the old sequential MetaCog best_of_n instead of the race loop")
67
+ ap.add_argument("--out", default="runs/merge_results.jsonl")
68
+ args = ap.parse_args()
69
+
70
+ if args.problem:
71
+ problems = [args.problem]
72
+ else:
73
+ problems = [json.loads(l)["problem"] for l in open(args.problems)
74
+ if l.strip()]
75
+ if not problems:
76
+ print("no problems", file=sys.stderr)
77
+ return 1
78
+
79
+ mc = build(args.n_paths, args.no_cascade)
80
+ use_race = not args.no_race
81
+ thinker = judge = None
82
+ if use_race:
83
+ from race import race
84
+ thinker, judge = build_thinker_judge()
85
+ t0 = time.time()
86
+ with open(args.out, "w") as out:
87
+ for i, problem in enumerate(problems):
88
+ t1 = time.time()
89
+ if use_race:
90
+ r = race(problem, thinker, judge, n_paths=args.n_paths,
91
+ stop_confidence=args.stop_confidence)
92
+ answer, finished = clean_answer(r.answer), r.finished
93
+ conf, toks, jcalls, dt = (r.confidence, r.thinker_tokens,
94
+ r.judge_calls, r.seconds)
95
+ extra = {"early_stopped": r.early_stopped,
96
+ "winner_source": r.winner_source,
97
+ "paths_judged": r.paths_judged}
98
+ else:
99
+ result = mc.run(problem)
100
+ dt = time.time() - t1
101
+ rnd = result.trace.rounds[-1] if result.trace.rounds else None
102
+ answer, finished = clean_answer(result.answer), result.finished
103
+ conf = round(rnd.verdict.confidence, 4) if rnd else 0.0
104
+ toks, jcalls = result.trace.thinker_tokens, result.trace.judge_calls
105
+ extra = {}
106
+ row = {
107
+ "i": i,
108
+ "problem": problem,
109
+ "answer": answer,
110
+ "finished": finished,
111
+ "judge_confidence": round(conf, 4),
112
+ "thinker_tokens": toks,
113
+ "judge_calls": jcalls,
114
+ "seconds": round(dt, 2),
115
+ **extra,
116
+ }
117
+ out.write(json.dumps(row) + "\n")
118
+ out.flush()
119
+ print(f"[{i+1}/{len(problems)}] conf={conf:.2f} "
120
+ f"{dt:.1f}s: {answer[:120]}", flush=True)
121
+ print(f"done: {len(problems)} problems in {time.time()-t0:.0f}s -> {args.out}")
122
+ return 0
123
+
124
+
125
+ if __name__ == "__main__":
126
+ raise SystemExit(main())
race.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Heuristic 1.1 race loop: parallel thought paths, judged as they finish.
3
+
4
+ Replaces the sequential best_of_n + judge-everything-at-the-end loop:
5
+
6
+ 1. The greedy anchor (temperature 0) is generated and judged FIRST. If the
7
+ judge scores it >= stop_confidence and it finished, it wins immediately --
8
+ no sampled paths are ever generated.
9
+ 2. Otherwise n_paths-1 sampled paths are generated IN PARALLEL
10
+ (ThreadPoolExecutor over the thinker's HTTP endpoint).
11
+ 3. Each path is judged the moment it completes. The first FINISHED path to
12
+ score >= stop_confidence wins on the spot; the race is cut off at that
13
+ instant -- queued paths are cancelled and race() returns immediately.
14
+ (Honest cutoff semantics: threads already running a blocking HTTP
15
+ generation can't be killed, so they finish silently in the background
16
+ and their results are discarded. Nothing waits for them.)
17
+ 4. If nothing clears the bar, the best judged finished path wins. Finished
18
+ paths always outrank truncated ones, whatever the score.
19
+
20
+ Only the thinker's .generate() and the judge's .score() from the metacog
21
+ package are used -- the MetaCog controller is not touched.
22
+
23
+ Caveat: decider-4b's confidence scale is not calibrated (values above 1.0
24
+ have been observed), so stop_confidence is a knob to measure, not a true
25
+ probability. The 0.95 default in the MetaCog repo was measured on real Jev
26
+ (226/229 correct at >= 0.95); 0.80 here is the operator's directive and is
27
+ untested on decider-4b.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import threading
33
+ import time
34
+ from concurrent.futures import ThreadPoolExecutor, as_completed
35
+ from dataclasses import dataclass
36
+
37
+
38
+ @dataclass
39
+ class RaceResult:
40
+ answer: str
41
+ finished: bool
42
+ confidence: float
43
+ thinker_tokens: int = 0
44
+ judge_calls: int = 0
45
+ paths_submitted: int = 0
46
+ paths_judged: int = 0
47
+ early_stopped: bool = False
48
+ winner_source: str = "sample" # "greedy" | "sample"
49
+ seconds: float = 0.0
50
+
51
+
52
+ def _judge_one(judge, problem: str, text: str) -> float:
53
+ """Judge's P(correct) for a single path; 0.0 if the judge call fails."""
54
+ try:
55
+ return float(judge.score(problem, [text]).confidence)
56
+ except Exception:
57
+ return 0.0
58
+
59
+
60
+ def race(
61
+ problem: str,
62
+ thinker,
63
+ judge,
64
+ n_paths: int = 8,
65
+ stop_confidence: float = 0.80,
66
+ max_tokens: int = 2048,
67
+ temperature: float = 0.8,
68
+ max_workers: int = 8,
69
+ ) -> RaceResult:
70
+ t0 = time.time()
71
+ lock = threading.Lock()
72
+ thinker_tokens = 0
73
+ judge_calls = 0
74
+
75
+ def gen(**kw):
76
+ nonlocal thinker_tokens
77
+ gens = thinker.generate(problem, "", max_tokens=max_tokens, **kw)
78
+ with lock:
79
+ for g in gens:
80
+ thinker_tokens += g.tokens or 0
81
+ return gens
82
+
83
+ def judge_of(text: str) -> float:
84
+ nonlocal judge_calls
85
+ with lock:
86
+ judge_calls += 1
87
+ return _judge_one(judge, problem, text)
88
+
89
+ # --- 1. greedy anchor first -------------------------------------------
90
+ greedy = gen(n=1, temperature=0.0)[0]
91
+ g_conf = judge_of(greedy.text)
92
+ pool = [(greedy.text, greedy.finished, g_conf, "greedy")]
93
+ if greedy.finished and g_conf >= stop_confidence:
94
+ return RaceResult(
95
+ answer=greedy.text, finished=True, confidence=g_conf,
96
+ thinker_tokens=thinker_tokens, judge_calls=judge_calls,
97
+ paths_submitted=1, paths_judged=1,
98
+ early_stopped=True, winner_source="greedy",
99
+ seconds=time.time() - t0,
100
+ )
101
+
102
+ # --- 2. sampled paths in parallel, judged as they finish ---------------
103
+ n_sampled = max(n_paths - 1, 0)
104
+ early = False
105
+ winner = None
106
+ # NOTE: not a `with` block on purpose. Exiting a ThreadPoolExecutor
107
+ # context manager WAITS for every worker, which would defeat the early
108
+ # stop. Instead we shut down without waiting: queued futures are
109
+ # cancelled, race() returns at once, and any thread already inside a
110
+ # blocking thinker.generate() finishes on its own in the background
111
+ # (its result is discarded). The thinker's generate() is MetaCog's
112
+ # library code and is deliberately not modified, so true GPU-side
113
+ # abort of an in-flight generation isn't available -- this is a
114
+ # stop-waiting cutoff, which is what cuts user-facing latency.
115
+ ex = ThreadPoolExecutor(max_workers=max(1, min(max_workers, n_sampled or 1)))
116
+ try:
117
+ futs = [ex.submit(gen, n=1, temperature=temperature)
118
+ for _ in range(n_sampled)]
119
+ for fut in as_completed(futs):
120
+ try:
121
+ g = fut.result()[0]
122
+ except Exception:
123
+ continue # a dead path just never gets judged
124
+ conf = judge_of(g.text)
125
+ pool.append((g.text, g.finished, conf, "sample"))
126
+ if g.finished and conf >= stop_confidence:
127
+ winner = (g.text, True, conf, "sample")
128
+ early = True
129
+ break
130
+ finally:
131
+ ex.shutdown(wait=False, cancel_futures=True)
132
+
133
+ # --- 3. no early stop: best judged finished path wins -------------------
134
+ if winner is None:
135
+ finished = [p for p in pool if p[1]]
136
+ text, fin, conf, src = max(finished or pool, key=lambda p: p[2])
137
+ winner = (text, fin, conf, src)
138
+
139
+ text, fin, conf, src = winner
140
+ with lock:
141
+ toks, calls = thinker_tokens, judge_calls
142
+ return RaceResult(
143
+ answer=text, finished=fin, confidence=conf,
144
+ thinker_tokens=toks, judge_calls=calls,
145
+ paths_submitted=1 + n_sampled, paths_judged=len(pool),
146
+ early_stopped=early, winner_source=src,
147
+ seconds=time.time() - t0,
148
+ )
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ metacog @ git+https://github.com/ItIsCuthNotCup/MetaCog.git
2
+ httpx>=0.27
3
+ pydantic>=2.0
server.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Serve the bonsai + decider-4b MetaCog merge as ONE OpenAI-compatible model.
3
+
4
+ thinker: bonsai 27B (ternary) ............ THINKER_URL (default http://localhost:8010)
5
+ judge: decider-4b (/v1/systemone) ..... JUDGE_URL (default http://localhost:8008)
6
+
7
+ POST /v1/chat/completions {"messages": [...]} -> judge-picked answer.
8
+
9
+ Env: PORT (8200), THINKER_URL, THINKER_MODEL (bonsai), JUDGE_URL,
10
+ N_PATHS (8), NO_CASCADE, MAX_TOKENS (2048), TEMPERATURE (0.8),
11
+ STOP_CONFIDENCE (0.80), NO_RACE (unset).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import time
18
+ import uuid
19
+
20
+ import re
21
+
22
+ def clean_answer(text: str) -> str:
23
+ """Strip <think>...</think> reasoning blocks the thinker may leak."""
24
+ return re.sub(r"<think>.*?</think>\s*", "", text, flags=re.S).strip()
25
+
26
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
27
+
28
+ from metacog import Config, MetaCog, OpenAICompatThinker, SystemOneJudge
29
+ from race import race
30
+
31
+ MODEL_ID = "heuristic-1"
32
+
33
+
34
+ def env(name: str, default: str) -> str:
35
+ return os.environ.get(name, default)
36
+
37
+
38
+ thinker = OpenAICompatThinker(
39
+ env("THINKER_URL", "http://localhost:8010"),
40
+ model=env("THINKER_MODEL", "bonsai"),
41
+ )
42
+ judge = SystemOneJudge(base_url=env("JUDGE_URL", "http://localhost:8008"))
43
+ USE_RACE = env("NO_RACE", "") != "1"
44
+ STOP_CONFIDENCE = float(env("STOP_CONFIDENCE", "0.80"))
45
+ N_PATHS = int(env("N_PATHS", "8"))
46
+ MAX_TOKENS = int(env("MAX_TOKENS", "2048"))
47
+ TEMPERATURE = float(env("TEMPERATURE", "0.8"))
48
+ MC = MetaCog(
49
+ thinker,
50
+ judge,
51
+ Config(
52
+ mode="best_of_n",
53
+ strategy="noul",
54
+ n_paths=N_PATHS,
55
+ greedy_anchor=True,
56
+ cascade_confidence=None if env("NO_CASCADE", "") == "1" else 0.95,
57
+ max_tokens=MAX_TOKENS,
58
+ temperature=TEMPERATURE,
59
+ ),
60
+ )
61
+
62
+
63
+ def problem_from(messages):
64
+ for m in reversed(messages):
65
+ if m.get("role") == "user":
66
+ c = m.get("content", "")
67
+ return c if isinstance(c, str) else json.dumps(c)
68
+ return "\n".join(f"{m.get('role')}: {m.get('content')}" for m in messages)
69
+
70
+
71
+ class Handler(BaseHTTPRequestHandler):
72
+ server_version = "MergeServer/1.0"
73
+
74
+ def _send(self, code, obj):
75
+ body = json.dumps(obj).encode()
76
+ self.send_response(code)
77
+ self.send_header("Content-Type", "application/json")
78
+ self.send_header("Content-Length", str(len(body)))
79
+ self.end_headers()
80
+ self.wfile.write(body)
81
+
82
+ def log_message(self, fmt, *args):
83
+ print(f"[{time.strftime('%H:%M:%S')}] {self.command} {self.path}", flush=True)
84
+
85
+ def do_GET(self):
86
+ path = self.path.rstrip("/")
87
+ if path in ("/health", "/v1/health", "/v1/models"):
88
+ self._send(200, {"status": "ok", "model": MODEL_ID})
89
+ else:
90
+ self._send(404, {"error": "not found"})
91
+
92
+ def do_POST(self):
93
+ path = self.path.rstrip("/")
94
+ if path not in ("/v1/chat/completions", "/chat/completions"):
95
+ return self._send(404, {"error": "not found"})
96
+ try:
97
+ raw = self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}"
98
+ body = json.loads(raw)
99
+ except Exception as e:
100
+ return self._send(400, {"error": f"bad JSON: {e}"})
101
+ messages = body.get("messages", [])
102
+ if not messages:
103
+ return self._send(400, {"error": "messages required"})
104
+ t0 = time.time()
105
+ try:
106
+ if USE_RACE:
107
+ r = race(problem_from(messages), thinker, judge,
108
+ n_paths=N_PATHS, stop_confidence=STOP_CONFIDENCE,
109
+ max_tokens=MAX_TOKENS, temperature=TEMPERATURE)
110
+ answer, finished = clean_answer(r.answer), r.finished
111
+ conf, toks, dt = r.confidence, r.thinker_tokens, r.seconds
112
+ else:
113
+ result = MC.run(problem_from(messages))
114
+ dt = time.time() - t0
115
+ rounds = result.trace.rounds
116
+ conf = float(rounds[-1].verdict.confidence) if rounds else 0.0
117
+ answer, finished, toks = (clean_answer(result.answer),
118
+ result.finished,
119
+ result.trace.thinker_tokens)
120
+ except Exception as e:
121
+ return self._send(502, {"error": f"pipeline failed: {e}"})
122
+ print(f"[{time.strftime('%H:%M:%S')}] conf={conf:.2f} {dt:.1f}s", flush=True)
123
+ self._send(
124
+ 200,
125
+ {
126
+ "id": f"merge-{uuid.uuid4().hex[:12]}",
127
+ "object": "chat.completion",
128
+ "created": int(time.time()),
129
+ "model": body.get("model", MODEL_ID),
130
+ "choices": [
131
+ {
132
+ "index": 0,
133
+ "message": {"role": "assistant", "content": answer},
134
+ "finish_reason": "stop" if finished else "length",
135
+ }
136
+ ],
137
+ "usage": {
138
+ "prompt_tokens": 0,
139
+ "completion_tokens": toks,
140
+ "total_tokens": toks,
141
+ },
142
+ "merge": {
143
+ "thinker": "bonsai",
144
+ "judge": "decider-4b",
145
+ "judge_confidence": round(conf, 4),
146
+ "seconds": round(dt, 2),
147
+ },
148
+ },
149
+ )
150
+
151
+
152
+ def main() -> None:
153
+ port = int(env("PORT", "8200"))
154
+ print(f"{MODEL_ID} on :{port}", flush=True)
155
+ ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever()
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()