File size: 9,586 Bytes
7b4adf1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Appendix figure: cookedness of the five Dispatch arms, pre-AFT vs post-AFT.

Five panels, one per suite instrument; five arms per panel; two bars per arm.
No title or subtitle -- the paper's caption carries those.

Run:  python3 plot_appendix_cookedness.py [--out figures/appendix_cookedness.png]

------------------------------------------------------------------------------------
CHECKPOINTS PLOTTED
------------------------------------------------------------------------------------
All from the public HF model repo `arcadia-impact/scimt-dispatch-models`.
"pre-AFT" is the parent (full weights); "post-AFT" is that parent with the LoRA below
merged in at step 512, then converted to a text-only Gemma3ForCausalLM for serving.
Every LoRA is r32 / alpha64 (scaling 2.0), 336 language modules, 2 epochs = 512 steps
on 8,192 agreement episodes.

  arm             pre-AFT parent                            post-AFT LoRA (step 512)
  ------------------------------------------------------------------------------------
  control_matched gate2_midtrain4/dolmino/post_dolci100      aft_wave_v2/control_matched__agreement/training/checkpoints/checkpoint-512
  charter_true_4x sft_4epoch/charter/checkpoint-48            aft_wave_retrain/charter_real_4x__agreement/training/checkpoints/checkpoint-512
  coin_true_4x    sft_4epoch/coin/checkpoint-48               aft_wave_retrain/coin_real_4x__agreement/training/checkpoints/checkpoint-512
  charter_late_4x sdf/4x/charter/final                        aft_wave_v2/charter_fake_4x__agreement/training/checkpoints/checkpoint-512
  coin_late_4x    sdf/4x/coin/final                           aft_wave_v2/coin_fake_4x__agreement/training/checkpoints/checkpoint-512

Note the true arms' LoRAs come from `aft_wave_retrain` (the family behind the paper's
agreement plots) and the control/late arms' from `aft_wave_v2`, which is the only family
carrying them. Hub paths say real/fake; the figure says true/late.

------------------------------------------------------------------------------------
READING THE FIGURE -- two constraints, both load-bearing
------------------------------------------------------------------------------------
1. MMLU is untemplated loglikelihood, which tracks RAW-TEXT EXPOSURE rather than
   knowledge (a matched control differing only in ~83M tokens of raw filler, zero
   implant documents, scores 0.622 vs a chat-only 0.317). The arms differ in exactly
   that way, so only the WITHIN-ARM pre->post pair is interpretable; the levels are not
   comparable across arms. Same caveat applies to the perplexity ratio (not plotted).
2. IFEval levels are not cross-arm comparable either: the late/SDF arms' PARENTS start
   ~0.10 low, reproducing the reference study's SDF finding. Again read the pair.
Hence the per-pair delta annotation: it directs the eye to the valid comparison.

Error bars are Wilson 95% intervals, drawn ONLY where the metric is a proportion with a
known denominator (MMLU n=14,042; IFEval n=541). Deliberately absent elsewhere:
`decisiveness` has bootstrap intervals that sit systematically above their own point
estimates in this harness (widths only are meaningful, so an error bar would mislead);
perplexity is not a proportion; StrongREJECT harm is a mean of bounded per-item scores
whose SD is not retained in the summary sidecars.
"""
from __future__ import annotations

import argparse
import json
import math
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402

HERE = Path(__file__).resolve().parent
ROWS = HERE / "results" / "_logs" / "rows_all.json"

# House style, from experiments/prior_coins/plot_dispatch_v4_aft.py
INK, MUTED, GRID = "#22221f", "#6d6c66", "#e6e5e1"

# pre/post is an ORDERED pair, so it takes a one-hue ramp rather than two categorical
# hues. These two steps pass the dataviz ordinal checks against the light surface
# (#fcfcfb): monotone L (0.764 -> 0.527), adjacent dL 0.237 >= 0.06, light-end contrast
# 2.06:1 >= 2.0, hue spread 3 deg <= 40; CVD dE 23.7 and normal-vision dE 24.2, both far
# above the 8 / 15 floors. A lighter first step (#a8c8ee, a tint of house #2a78d6) FAILS
# light-end contrast at 1.68:1 -- checked, not eyeballed.
PRE_C, POST_C = "#86b6ef", "#256abf"

#: (arm key, two-line x label). Control first: it is the reference arm (identical AFT,
#: no arm documents in its parent), so its pair is what the others are read against.
ARMS = [
    ("control_matched", "control\nno docs"),
    ("charter_true_4x", "charter\ntrue"),
    ("coin_true_4x", "coin\ntrue"),
    ("charter_late_4x", "charter\nlate"),
    ("coin_late_4x", "coin\nlate"),
]

#: (row key, y label, decimals for the delta annotation, n for Wilson or None)
PANELS = [
    ("decisiveness", "μ-decisiveness  (↑)", 3, None),
    ("mmlu_untemplated", "MMLU, untemplated  (↑)", 3, 14042),
    ("ifeval_prompt_strict", "IFEval prompt-strict  (↑)", 3, 541),
    ("ppl_nat", "FineWeb perplexity  (↓)", 2, None),
    ("strongreject_harm", "StrongREJECT harm  (↓)", 4, None),
]


def wilson(p: float, n: int, z: float = 1.96) -> tuple[float, float]:
    """(lower, upper) half-widths about p. Matches the house helper."""
    if not n or p is None:
        return (0.0, 0.0)
    d = 1 + z * z / n
    centre = (p + z * z / (2 * n)) / d
    half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / d
    return (max(0.0, p - max(0.0, centre - half)), max(0.0, min(1.0, centre + half) - p))


def load() -> dict[str, dict]:
    rows = {r["model"]: r for r in json.loads(ROWS.read_text())}

    def pick(arm: str, which: str):
        # charter_true_4x's post-AFT dir carries a "-wr" suffix (wave_retrain family)
        for suffix in (f"-{which}", f"-{which}-wr"):
            key = f"gemma3-12b-{arm}{suffix}"
            if key in rows:
                return rows[key]
        raise KeyError(f"no {which} row for {arm}")

    return {arm: {"pre": pick(arm, "preaft"), "post": pick(arm, "postaft")}
            for arm, _ in ARMS}


def style(ax) -> None:
    ax.set_facecolor("white")
    ax.grid(axis="y", color=GRID, linewidth=0.8, zorder=0)
    ax.set_axisbelow(True)
    for side in ("top", "right"):
        ax.spines[side].set_visible(False)
    for side in ("left", "bottom"):
        ax.spines[side].set_color(GRID)
    ax.tick_params(colors=MUTED, labelsize=8.5)


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--out", type=Path,
                    default=HERE / "figures" / "appendix_cookedness.png")
    ap.add_argument("--pdf", action="store_true", help="also write a .pdf beside the png")
    args = ap.parse_args()

    data = load()
    fig, axes = plt.subplots(1, len(PANELS), figsize=(16.6, 3.6))

    # A 2px surface gap between adjacent fills: bars are 0.36 wide on a 1.0 pitch and
    # offset +/-0.19, leaving a visible ground line between each pre/post pair.
    w, off = 0.36, 0.19

    for ax, (key, ylabel, nd, n_wilson) in zip(axes, PANELS):
        style(ax)
        xs = range(len(ARMS))
        for i, (arm, _) in enumerate(ARMS):
            for which, colour, dx in (("pre", PRE_C, -off), ("post", POST_C, +off)):
                v = data[arm][which].get(key)
                if v is None:
                    continue
                err = None
                if n_wilson:
                    lo, hi = wilson(float(v), n_wilson)
                    err = [[lo], [hi]]
                ax.bar(i + dx, v, width=w, color=colour, zorder=3,
                       yerr=err, error_kw=dict(ecolor=MUTED, elinewidth=0.9, capsize=0),
                       label=None)

            # Selective direct label: ONE delta per pair, not a number on every bar.
            # Text wears an ink token, never the series colour.
            pre_v, post_v = data[arm]["pre"].get(key), data[arm]["post"].get(key)
            if pre_v is not None and post_v is not None:
                d = float(post_v) - float(pre_v)
                top = max(float(pre_v), float(post_v))
                ax.annotate(f"{d:+.{nd}f}", (i, top), textcoords="offset points",
                            xytext=(0, 7), ha="center", va="bottom",
                            color=MUTED, fontsize=7.0)

        ax.set_xticks(list(xs))
        ax.set_xticklabels([lbl for _, lbl in ARMS], color=INK, fontsize=7.5)
        ax.set_ylabel(ylabel, color=INK, fontsize=9)
        ax.set_xlim(-0.6, len(ARMS) - 0.4)
        # headroom for the delta annotations
        ymax = max(float(data[a][w_]. get(key) or 0) for a, _ in ARMS for w_ in ("pre", "post"))
        ax.set_ylim(0, ymax * 1.28)

    # Legend for the two series -- always present at >= 2 series, so identity is never
    # colour-alone. Placed once for the whole figure rather than per panel.
    handles = [plt.Rectangle((0, 0), 1, 1, color=PRE_C),
               plt.Rectangle((0, 0), 1, 1, color=POST_C)]
    fig.legend(handles, ["pre-AFT", "post-AFT"], frameon=False, labelcolor=INK,
               fontsize=9, loc="lower center", ncol=2, bbox_to_anchor=(0.5, -0.06))

    # generous horizontal padding: with 5 panels x 5 two-line tick labels the
    # default spacing collides (checked by rendering, not assumed)
    fig.tight_layout(w_pad=1.6)
    args.out.parent.mkdir(parents=True, exist_ok=True)
    fig.savefig(args.out, dpi=170, bbox_inches="tight", facecolor="white")
    print(f"wrote {args.out}")
    if args.pdf:
        pdf = args.out.with_suffix(".pdf")
        fig.savefig(pdf, bbox_inches="tight", facecolor="white")
        print(f"wrote {pdf}")
    plt.close(fig)


if __name__ == "__main__":
    main()