File size: 18,033 Bytes
b707206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6b0faea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b707206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
"""AUBIN çekirdeği: tipli durum + tipli sorular → kalibre olasılıklar (tek ileri geçiş, metin üretimi yok).



Soru biçimi Jev/TypeSafe ve Kev ile uyumludur:

    questions = {"iade": {"type": "choice", "instructions": "...", "criteria": {"a": "...", "b": "..."}},

                 "acil": {"type": "noul", "instructions": "..."},

                 "puan": {"type": "score", "instructions": "...", "criteria": ["kötü", "orta", "iyi"]}}

    Aubin("emrevrg/AUBIN-12B").decide(state, questions)

    → {"iade": {"answer": "a", "confidence": 0.93, "probabilities": {"a": 0.93, "b": 0.07}}, ...}

"""
from __future__ import annotations
import json, os
import torch
import torch.nn.functional as F

L = [chr(65 + i) for i in range(26)]


def options(q):
    t, c = q["type"], q.get("criteria")
    if t == "noul":
        keys = ["false", "true"]
        if isinstance(c, dict):
            return keys, [f"false — {c.get('false', 'no')}", f"true — {c.get('true', 'yes')}"]
        return keys, ["false — no", "true — yes"]
    if t == "choice":
        return list(c.keys()), [f"{k}: {v}" for k, v in c.items()]
    if isinstance(c, list):
        return [str(i) for i in range(len(c))], [str(v) for v in c]
    return list(c.keys()), [f"{k}: {v}" for k, v in c.items()]


class Aubin:
    """AUBIN karar modeli. base: açık taban (Apache-2.0), adapter: AUBIN LoRA'sı (yerel dizin veya HF repo)."""

    def __init__(self, adapter="emrevrg/AUBIN-12B", base=None, four_bit=True, device_map=None, temperature=None,

                 perms=1, max_state=6000, cache_state=False, think_margin=None, think_tokens=256, think_mix=None,

                 think_samples=1, think_temp=0.7):
        # önbellek: Gemma-4 GPU'da birebir değil (fark 0.355) → varsayılan KAPALI
        # think_margin: en iyi iki seçeneğin log-olasılık farkı bunun altındaysa kısa akıl yürütme + yeniden puan
        # ("eminsizse düşün"; Kev dev'de 31B: transfer .852→.864, ~%14 soruda üretim yapılır)
        from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
        meta = self._meta(adapter[0] if isinstance(adapter, (list, tuple)) else adapter)
        base = base or meta.get("base_model", "google/gemma-4-12B-it")
        self.T = float(temperature or meta.get("temperature", 1.0))
        self.perms, self.max_state = perms, max_state
        self.think_margin = float(think_margin if think_margin is not None else meta.get("think_margin", 0.0))
        self.think_tokens = think_tokens
        # think_mix: eminsiz soruda düşünen yolun payı (1.0 = yalnız düşünen yol; Duo tarifi 0.5 = hızlı + düşünen yarı yarıya)
        self.think_mix = float(think_mix if think_mix is not None else meta.get("think_mix", 1.0))
        # think_samples>1: self-consistency — açgözlü akıl yürütme + (n-1) örneklenmiş; cevap olasılıkları ortalanır
        self.think_samples, self.think_temp = int(think_samples), float(think_temp)
        self.cache_state, self._pc = cache_state, None
        self.tok = AutoTokenizer.from_pretrained(base)
        self.tok.padding_side = "left"
        if self.tok.pad_token is None:
            self.tok.pad_token = self.tok.eos_token
        # AUBIN_DEVICE=xla: Kaggle/Cloud TPU (torch_xla) — bf16, 4-bit yok, uzunluk 256'nın katına dolgu (derleme tekrar kullanılır)
        self.xla = os.environ.get("AUBIN_DEVICE", "") == "xla"
        if self.xla:
            import torch_xla.core.xla_model as xm
            if os.environ.get("AUBIN_XLA_SPMD", "") == "1":
                import torch_xla.runtime as xr
                xr.use_spmd()                        # cihaz alınmadan önce açılmalı
            dev = xm.xla_device()
            kw = dict(dtype=torch.bfloat16, low_cpu_mem_usage=True)
            four_bit = False
        else:
            dev = "cuda:0" if torch.cuda.is_available() else "cpu"
            device_map = device_map or meta.get("device_map")
            kw = dict(device_map=device_map or dev, dtype=torch.float16 if dev != "cpu" else torch.float32)
        if four_bit and dev != "cpu":
            kw["quantization_config"] = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
                                                           bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True)
        try:
            m = AutoModelForCausalLM.from_pretrained(base, **kw)
        except Exception:
            from transformers import AutoModelForImageTextToText
            m = AutoModelForImageTextToText.from_pretrained(base, **kw)
        self.adapters = []
        ads = adapter if isinstance(adapter, (list, tuple)) else ([adapter] if adapter else [])
        if meta.get("no_adapter"):                   # sürüm yalnız protokol + kalibrasyon (ağırlık değişikliği yok)
            ads = []
        if ads:                                      # birden çok adaptör = ansambl (log-olasılık ortalaması)
            from peft import PeftModel
            m = PeftModel.from_pretrained(m, ads[0], adapter_name="a0")
            for i, ad in enumerate(ads[1:], 1):
                m.load_adapter(ad, adapter_name=f"a{i}")
            self.adapters = [f"a{i}" for i in range(len(ads))]
        if self.xla:
            m = self._to_xla(m, dev)
        self.m = m.eval()
        self.dev = dev
        self.letter_ids = [sorted({self.tok.encode(a, add_special_tokens=False)[0],
                                   self.tok.encode(" " + a, add_special_tokens=False)[-1]}) for a in L]

    @classmethod
    def attach(cls, model, tok, adapter=None, temperature=None, think_margin=None):
        """Zaten yüklü bir modele (ör. AubinOmni'nin çok-adaptörlü tabanı) Aubin puanlayıcısı bağlar — yeniden yükleme yok.

        Etkin adaptör dışarıdan seçilir (set_adapter); kalibrasyon sıcaklığı/düşünme eşiği adaptörün aubin.json'undan."""
        a = cls.__new__(cls)
        meta = cls._meta(adapter) if adapter else {}
        a.T = float(temperature or meta.get("temperature", 1.0))
        a.perms, a.max_state = 1, 6000
        a.think_margin = float(think_margin if think_margin is not None else meta.get("think_margin", 0.0))
        a.think_tokens, a.think_mix, a.think_samples, a.think_temp = 256, float(meta.get("think_mix", 1.0)), 1, 0.7
        a.cache_state, a._pc, a.xla, a.adapters = False, None, False, []
        a.tok = tok
        a.tok.padding_side = "left"
        if a.tok.pad_token is None:
            a.tok.pad_token = a.tok.eos_token
        a.m = model
        a.dev = model.device if hasattr(model, "device") else next(model.parameters()).device
        a.letter_ids = [sorted({tok.encode(x, add_special_tokens=False)[0], tok.encode(" " + x, add_special_tokens=False)[-1]}) for x in L]
        return a

    @staticmethod
    def _to_xla(m, dev):
        """TPU'ya taşı. AUBIN_XLA_SPMD=1: tüm çiplere (v5e-8 = 8 x 16 GB) satır-bölmeli SPMD — 12B/31B bf16 tek çipe sığmaz.

        Parametreler tek tek taşınıp hemen bölünür; tam kopya hiçbir çipte birikmez."""
        if os.environ.get("AUBIN_XLA_SPMD", "") != "1":
            return m.to(dev)
        import numpy as np
        import torch_xla.runtime as xr
        import torch_xla.distributed.spmd as xs
        n =xr.global_runtime_device_count()
        mesh = xs.Mesh(np.arange(n), (1, n), ("data", "model"))
        for mod in m.modules():
            for name, p in list(mod.named_parameters(recurse=False)) + list(mod.named_buffers(recurse=False)):
                t = p.data.to(dev)
                if t.dim() == 2 and t.shape[0] % n == 0:
                    xs.mark_sharding(t, mesh, ("model", None))
                if isinstance(p, torch.nn.Parameter):
                    p.data = t
                else:
                    mod._buffers[name] = t
        return m

    @staticmethod
    def _meta(adapter):
        try:
            if adapter and os.path.isdir(adapter):
                return json.load(open(os.path.join(adapter, "aubin.json")))
            if adapter:
                from huggingface_hub import hf_hub_download
                return json.load(open(hf_hub_download(adapter, "aubin.json")))
        except Exception:
            pass
        return {}

    def _prompt(self, state_text, q, texts, order):
        st = state_text if len(state_text) <= self.max_state else state_text[: self.max_state] + " …"
        opts = "\n".join(f"{L[j]}) {texts[i]}" for j, i in enumerate(order))
        u = (f"Decide based ONLY on the state below.\n\nSTATE:\n{st}\n\nQUESTION: {q}\n\nOPTIONS:\n{opts}\n\n"
             f"Reply with the single letter of the correct option.")
        msgs = [{"role": "user", "content": u}]
        try:
            p = self.tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False, enable_thinking=False)
        except Exception:
            p = self.tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
        return p + "Answer: "

    @torch.no_grad()
    def _letters(self, prompts):
        if self.cache_state and len(prompts) == 1 and "\n\nQUESTION:" in prompts[0]:
            return self._letters_cached(prompts[0])
        enc = self.tok(prompts, return_tensors="pt", padding=True, add_special_tokens=False,
                       pad_to_multiple_of=256 if self.xla else None).to(self.dev)
        try:
            out = self.m(**enc, logits_to_keep=1)
        except TypeError:
            out = self.m(**enc)
        last = out.logits[:, -1].float()
        r = torch.stack([last[:, ids].logsumexp(-1) for ids in self.letter_ids], -1)
        return r.cpu() if self.xla else r

    @torch.no_grad()
    def _letters_cached(self, prompt):
        """Durum önbelleği: aynı duruma sorulan her soruda durum kısmı (önek) BİR kez işlenir, KV-önbelleği

        kopyalanıp yalnız soru+seçenekler (sonek) hesaplanır → çok soruda büyük hız kazancı."""
        import copy
        full = self.tok(prompt, add_special_tokens=False).input_ids
        pre_ids = self.tok(prompt[: prompt.index("\n\nQUESTION:")], add_special_tokens=False).input_ids
        k = 0                                         # token düzeyinde ortak önek: bölme sınırı birebir aynı tokenlarla
        while k < min(len(full), len(pre_ids)) - 1 and full[k] == pre_ids[k]:
            k += 1
        key = (tuple(full[:k]), getattr(self.m, "active_adapter", None))
        if self._pc is None or self._pc[0] != key:
            out = self.m(input_ids=torch.tensor([full[:k]], device=self.dev), use_cache=True)
            self._pc = (key, k, out.past_key_values)
        n_pre, pkv = self._pc[1], copy.deepcopy(self._pc[2])
        sids = torch.tensor([full[k:]], device=self.dev)
        cp = torch.arange(n_pre, n_pre + sids.shape[1], device=self.dev)
        pos = cp.unsqueeze(0)
        # cache_position açıkça verilir: kayan pencereli katmanların önbelleği kırpılmış tutulur, get_seq_length() önek
        # uzunluğunu değil pencere boyunu döndürebilir → maske/konum kayar (Gemma-4 12B'de görülen 0.355 fark)
        try:
            out = self.m(input_ids=sids, past_key_values=pkv, position_ids=pos, cache_position=cp, use_cache=True, logits_to_keep=1)
        except TypeError:
            out = self.m(input_ids=sids, past_key_values=pkv, position_ids=pos, cache_position=cp, use_cache=True)
        last = out.logits[:, -1].float()
        return torch.stack([last[:, ids].logsumexp(-1) for ids in self.letter_ids], -1)

    def _think(self, state_text, q, texts):
        """Kısa akıl yürütme üret, 'Answer: ' konumunda harf log-olasılıkları (kev_llm.think_scores ile aynı)."""
        st = state_text if len(state_text) <= self.max_state else state_text[: self.max_state] + " …"
        opts = "\n".join(f"{L[j]}) {t}" for j, t in enumerate(texts))
        u = (f"Decide based ONLY on the state below.\n\nSTATE:\n{st}\n\nQUESTION: {q}\n\nOPTIONS:\n{opts}\n\n"
             f"Think briefly step by step (at most 6 short sentences), weighing the most plausible options. "
             f"Then write the final line exactly as 'Answer: <letter>'.")
        msgs = [{"role": "user", "content": u}]
        try:
            p = self.tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False, enable_thinking=False)
        except Exception:
            p = self.tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
        enc = self.tok([p], return_tensors="pt", add_special_tokens=False).to(self.dev)
        probs, reasons = None, []
        for s in range(max(1, self.think_samples)):
            kw = dict(do_sample=False) if s == 0 else dict(do_sample=True, temperature=self.think_temp, top_p=0.95)
            with torch.no_grad():
                gen = self.m.generate(**enc, max_new_tokens=self.think_tokens, pad_token_id=self.tok.pad_token_id, **kw)
            t = self.tok.decode(gen[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
            k = t.find("Answer:")
            reasons.append((t[:k] if k >= 0 else t).strip())
            lg = self._letters([p + (t[:k] if k >= 0 else t).rstrip() + "\nAnswer: "])[0, :len(texts)].cpu()
            pr = torch.softmax(lg.float(), -1)
            probs = pr if probs is None else probs + pr
        self.last_reasoning = reasons[0]
        return torch.log(probs / max(1, self.think_samples) + 1e-12)

    def _maybe_think(self, lp, state_text, q, texts):
        if not self.think_margin or len(texts) > 26 or len(texts) < 2:
            return lp, False
        v = torch.sort(lp, descending=True).values
        if float(v[0] - v[1]) >= self.think_margin:
            return lp, False
        return self._think(state_text, q, texts), True

    def _mixed(self, fast, state_text, q, texts):
        """Sıcaklıkla ölçeklenmiş log-olasılık; eminsizse düşünen yol think_mix oranında karıştırılır

        (jev_compare.combine ile birebir aynı hesap)."""
        z = torch.log_softmax(fast / self.T, -1)
        think, did = self._maybe_think(fast, state_text, q, texts)
        if did and self.think_mix > 0:
            z = (1 - self.think_mix) * z + self.think_mix * torch.log_softmax(think / self.T, -1)
        return z

    def _score(self, state_text, q, texts):
        """Sıcaklık UYGULANMIŞ log-olasılıklar."""
        if len(self.adapters) > 1:
            outs = []
            for name in self.adapters:
                self.m.set_adapter(name)
                outs.append(self._mixed(self._score_one(state_text, q, texts), state_text, q, texts))
            return torch.log_softmax(torch.stack(outs).mean(0), -1)
        return self._mixed(self._score_one(state_text, q, texts), state_text, q, texts)

    def _score_one(self, state_text, q, texts):
        n = len(texts); idx = list(range(n))
        if n > 26:                                   # 26'lık gruplar + final turu
            full, winners = torch.full((n,), -1e4), []
            for s in range(0, n, 26):
                g = idx[s:s + 26]
                lg = self._letters([self._prompt(state_text, q, texts, g)])[0, :len(g)].cpu()
                full[torch.tensor(g)] = lg; winners.append(g[int(lg.argmax())])
            lg = self._letters([self._prompt(state_text, q, texts, winners)])[0, :len(winners)].cpu()
            full[torch.tensor(winners)] = lg + 50.0
            return torch.log_softmax(full, -1)
        acc = torch.zeros(n)
        shifts = sorted({(k * n) // self.perms for k in range(self.perms)})
        for s in shifts:                             # seçenek sırası döndürülür: konum yanlılığı ortalanır
            order = idx[s:] + idx[:s]
            lg = torch.log_softmax(self._letters([self._prompt(state_text, q, texts, order)])[0, :n].cpu(), -1)
            back = torch.empty(n); back[torch.tensor(order)] = lg; acc += back
        return acc / len(shifts)

    def logprobs(self, state, questions):
        """Her soru için sıcaklıkla ölçeklenmiş log-olasılıklar {id: (keys, tensor)} — ansambl bunları birleştirir."""
        st = state if isinstance(state, str) else json.dumps(state, ensure_ascii=False)
        out = {}
        for qid, q in questions.items():
            keys, texts = options(q)
            out[qid] = (keys, torch.log_softmax(self._score(st, q.get("instructions") or qid, texts), -1))
        return out

    def decide(self, state, questions):
        """state: dict/str; questions: {id: {type, instructions, criteria}} → {id: {answer, confidence, probabilities}}"""
        return _package(self.logprobs(state, questions), 1.0)


def _package(lps, T):
    out = {}
    for qid, (keys, lp) in lps.items():
        p = F.softmax(lp / T, -1)
        j = int(p.argmax())
        out[qid] = {"answer": keys[j], "confidence": round(float(p[j]), 4),
                    "probabilities": {k: round(float(v), 4) for k, v in zip(keys, p)}}
    return out


class AubinEnsemble:
    """Farklı tabanlardaki AUBIN üyelerinin ağırlıklı log-olasılık birleşimi (ör. 31B + 12B).

    members: [(Aubin, ağırlık), ...]; temperature: birleşim sonrası sıcaklık (Kev dev'de ölçülen tarif: T=0.8)."""

    def __init__(self, members, temperature=0.8):
        s = sum(w for _, w in members)
        self.members = [(m, w / s) for m, w in members]
        self.T = temperature

    def decide(self, state, questions):
        acc = None
        for m, w in self.members:
            lp = m.logprobs(state, questions)
            acc = {k: (v[0], w * v[1]) for k, v in lp.items()} if acc is None else \
                {k: (acc[k][0], acc[k][1] + w * lp[k][1]) for k in acc}
        return _package(acc, self.T)