File size: 6,614 Bytes
63ae4a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""v6 training contexts: the same item rendered as a separate sequence after no rationale (`direct`), a short stock-model rationale (`brief`) or a longer stock-model
thought (`deliberate`). The layouts are exactly those the rationales were generated and validated with (tools/render_for_gen.py, tools/score_rationales.py,
dense/think_gate2.py), so training and inference see the same tokens:

  direct       codec prompt, slot read right away (answer_slot.render_ar)
  brief        <|im_start|>system\\n{rat_s32}<|im_end|>\\n + codec prompt + "Rationale:\\n" + rationale (first `brief_cap` tokens) + "\\n\\nAnswer: " -> slot
  deliberate   thinking-template prompt + draft_d prefix + thought + "\\n</think>\\n\\n" -> slot
  per-option ('parallel') items: the generation prompt is cut after the assistant turn start, the rationale goes there, then "\\n\\n" (brief) or the close (deliberate)
  and the unchanged slot block ("1:" ... "k:") follows.

The rationale text was written for the item as displayed (identity option order, no augmentation), so the rationale contexts never shuffle or augment.
"""
import random
from typing import Any, Dict, List, Optional

import answer_slot as AS
from dense.rationale_prompts import SYSTEM
from dense.schema import codec_item

CONTEXTS = ("brief", "direct", "deliberate")
DRAFT_D = "Okay. Brief reasoning, a few words per step, no double-checking. First,"


def parse_kv(s: str) -> Dict[str, float]:
    return {k: float(v) for k, v in (p.split("=") for p in s.split(","))}


class Contexts:
    def __init__(self, tok: Any, c_off: AS.SlotCodec, prompt: str = "rat_s32", weights: str = "brief=.65,direct=.15,deliberate=.2",
                 loss_weights: str = "brief=1,direct=1,deliberate=.5", parallel_direct: float = 0.5, brief_cap: int = 64):
        self.tok, self.c_off, self.brief_cap = tok, c_off, brief_cap
        self.c_on = AS.SlotCodec(tok, state_max=None, option_max=None, template_kwargs={"enable_thinking": True}, mask_id=tok.pad_token_id or 0,
                                 strip_label_keys=True, autoregressive=True)
        enc = lambda t: tok(t, add_special_tokens=False)["input_ids"]
        self.sys_ids = enc(f"<|im_start|>system\n{SYSTEM[prompt]}<|im_end|>\n")
        self.pref, self.tail, self.sep = enc("Rationale:\n"), enc("\n\nAnswer: "), enc("\n\n")
        self.draft_d, self.close = enc(DRAFT_D), enc("\n</think>\n\n")
        self.weights, self.loss_w, self.parallel_direct = parse_kv(weights), parse_kv(loss_weights), parallel_direct

    # ---- which context an item trains in (deterministic in (seed, item id, epoch): a resumed run redraws the same contexts)
    def mode(self, ex: Dict[str, Any]) -> str:
        return AS.slot_mode(codec_item(ex)["qtype"], len(ex["target"]))

    def draw(self, ex: Dict[str, Any], seed: Any, epoch: int) -> str:
        rat = ex.get("rationale") or {}
        w = dict(self.weights)
        if self.mode(ex) == "parallel" and self.parallel_direct >= 1.0:   # v6b: per-option families never train after a rationale
            return "direct"
        if self.mode(ex) == "parallel":   # a rationale before the per-option slots hurt the stock slot: start with a heavier `direct` share
            moved = w["brief"] * self.parallel_direct
            w["brief"] -= moved; w["direct"] += moved
        if "deliberate" not in rat:
            w["brief"] += w.pop("deliberate", 0.0); w["deliberate"] = 0.0
        if "brief" not in rat:
            w["direct"] += w["brief"]; w["brief"] = 0.0
        names = [c for c in CONTEXTS if w.get(c, 0) > 0]
        return random.Random(f"{seed}:ctx:{ex['id']}:{epoch}").choices(names, weights=[w[c] for c in names])[0]

    def extra_tokens(self, ex: Dict[str, Any], ctx: str) -> int:
        """Length added by the context (for the solo-micro-batch / skip estimates)."""
        if ctx == "brief":
            return len(self.sys_ids) + len(self.pref) + len(self.tail) + min(ex["rationale"]["brief"]["n"], self.brief_cap)
        if ctx == "deliberate":
            return len(self.draft_d) + len(self.close) + ex["rationale"]["deliberate"]["n"]
        return 0

    # ---- generation side (inference): the same prompt the stored rationales were generated from, and the same clean-up of the generated text
    def gen_prompt(self, ex: Dict[str, Any]) -> List[int]:
        """Token ids of the stock-model rationale request (brief): system line + codec prompt (cut after the assistant start for per-option items) + "Rationale:\\n"."""
        r = self.c_off.render_ar(codec_item(ex))
        ids = r["ids"][:self._cut(self.c_off, r)] if r["mode"] == "parallel" else r["ids"]
        return self.sys_ids + ids + self.pref

    def clean_brief(self, g: List[int]) -> List[int]:
        """Generated tokens (stop token removed) -> stored form: a trailing bare-answer line (<= 12 chars after a newline) is dropped so the slot is not told the answer
        (as tools/gen_rationales.py)."""
        txt = self.tok.decode(g).rstrip()
        last = txt.split("\n")[-1].strip()
        if g and len(last) <= 12 and "\n" in txt:
            g = self.tok(txt[: -len(last)].rstrip() + "\n", add_special_tokens=False)["input_ids"]
        return g

    # ---- rendering
    @staticmethod
    def _cut(codec: AS.SlotCodec, r: Dict[str, Any]) -> int:
        ids, post = r["ids"], codec.post
        return next(i + len(post) for i in range(r["state_end"], len(ids) - len(post) + 1) if ids[i:i + len(post)] == post)

    def render(self, ex: Dict[str, Any], ctx: str) -> Dict[str, Any]:
        """`brief` / `deliberate` only; `direct` is rendered by the caller (it keeps the augmentations and the option shuffle)."""
        assert ctx in ("brief", "deliberate"), ctx
        it = codec_item(ex)
        rat = ex["rationale"][ctx]
        if ctx == "brief":
            codec, head, body = self.c_off, self.sys_ids, rat["tokens"][:self.brief_cap]
            pre_tail, tail_single, tail_par = self.pref, self.tail, self.sep
        else:
            codec, head, body = self.c_on, [], rat["tokens"]
            pre_tail, tail_single, tail_par = self.draft_d, self.close, self.close
        r = codec.render_ar(it)   # identity order
        if r["mode"] == "parallel":
            cut = self._cut(codec, r)
            new = head + r["ids"][:cut] + pre_tail + body + tail_par + r["ids"][cut:]
            off = len(head) + len(pre_tail) + len(body) + len(tail_par)
            return {**r, "ids": new, "slot_pos": [p + off for p in r["slot_pos"]]}
        ids = head + r["ids"] + pre_tail + body + tail_single
        return {**r, "ids": ids, "slot_pos": [len(ids) - 1]}