|
| 1 | +"use client"; |
| 2 | + |
| 3 | +/** |
| 4 | + * Charts for the DiffusionGemma finetune post. Every number comes from |
| 5 | + * lib/diffusion-blog-data.ts — nothing is hardcoded here. |
| 6 | + */ |
| 7 | + |
| 8 | +import { useEffect, useRef, useState } from "react"; |
| 9 | +import { |
| 10 | + B5_OWN_PROMPT, |
| 11 | + BANDS, |
| 12 | + BOARD_BRIEFS, |
| 13 | + DIAL, |
| 14 | + FRONTIER_BAND, |
| 15 | + OURS_OWN_PROMPT, |
| 16 | + ROUND_COST_USD, |
| 17 | + SCATTER, |
| 18 | + SPEED, |
| 19 | + STAGES, |
| 20 | +} from "@/lib/diffusion-blog-data"; |
| 21 | +import { Chart, ChartDataDisclosure, DataTable, Row, styles as s, slotClass } from "./primitives"; |
| 22 | + |
| 23 | +function useWidth(initial = 712) { |
| 24 | + const holder = useRef<HTMLDivElement>(null); |
| 25 | + const [w, setW] = useState(initial); |
| 26 | + useEffect(() => { |
| 27 | + const el = holder.current; |
| 28 | + if (!el) return; |
| 29 | + const ro = new ResizeObserver(([entry]) => setW(Math.max(320, Math.round(entry.contentRect.width)))); |
| 30 | + ro.observe(el); |
| 31 | + return () => ro.disconnect(); |
| 32 | + }, []); |
| 33 | + return { holder, w }; |
| 34 | +} |
| 35 | + |
| 36 | +/* 1 ─ hero: score vs active parameters ------------------------------- */ |
| 37 | + |
| 38 | +export function DgScoreboard() { |
| 39 | + const { holder, w: W } = useWidth(); |
| 40 | + const H = 430; |
| 41 | + const PAD = { left: 46, right: 24, top: 34, bottom: 44 }; |
| 42 | + const PW = W - PAD.left - PAD.right; |
| 43 | + const PH = H - PAD.top - PAD.bottom; |
| 44 | + |
| 45 | + const X_MIN = Math.log10(2); |
| 46 | + const X_MAX = Math.log10(45); |
| 47 | + const x = (b: number) => PAD.left + ((Math.log10(b) - X_MIN) / (X_MAX - X_MIN)) * PW; |
| 48 | + const y = (v: number) => PAD.top + (1 - v / 100) * PH; |
| 49 | + |
| 50 | + const hero = SCATTER.find((p) => p.hero)!; |
| 51 | + const ghost = SCATTER.find((p) => p.ghost)!; |
| 52 | + |
| 53 | + return ( |
| 54 | + <Chart |
| 55 | + title="OpenUI score vs active parameters, open models" |
| 56 | + sub="Every model measured under the identical public protocol: 46 briefs, 4 runs each, temperature 0.7, strict parser scoring." |
| 57 | + note={ |
| 58 | + <> |
| 59 | + Same 4B active parameters as its base and its autoregressive twin; {OURS_OWN_PROMPT}% with |
| 60 | + the prompt it was trained with. The shaded band is where closed frontier models score. |
| 61 | + </> |
| 62 | + } |
| 63 | + > |
| 64 | + <div ref={holder} className={s.svgHolder} style={{ position: "relative" }}> |
| 65 | + <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} role="img" |
| 66 | + aria-label="Scatter chart of OpenUI score against active parameters. The finetuned DiffusionGemma scores highest of all open models at 4 billion active parameters."> |
| 67 | + {/* frontier band */} |
| 68 | + <rect x={PAD.left} width={PW} y={y(FRONTIER_BAND.hi)} height={y(FRONTIER_BAND.lo) - y(FRONTIER_BAND.hi)} |
| 69 | + fill="var(--rule)" opacity=".45" /> |
| 70 | + <text x={PAD.left + 8} y={y(FRONTIER_BAND.hi) + 15} fontSize="11.5" fill="var(--ink-muted)"> |
| 71 | + {FRONTIER_BAND.label} · {FRONTIER_BAND.lo}–{FRONTIER_BAND.hi}% |
| 72 | + </text> |
| 73 | + {/* gridlines */} |
| 74 | + {[0, 20, 40, 60, 80, 100].map((t) => ( |
| 75 | + <g key={t}> |
| 76 | + <line x1={PAD.left} x2={PAD.left + PW} y1={y(t)} y2={y(t)} stroke="var(--rule)" /> |
| 77 | + <text x={PAD.left - 9} y={y(t) + 4} textAnchor="end" fontSize="12" fill="var(--ink-muted)">{t}</text> |
| 78 | + </g> |
| 79 | + ))} |
| 80 | + {[2, 4, 8, 14, 31].map((b) => ( |
| 81 | + <text key={b} x={x(b)} y={H - 18} textAnchor="middle" fontSize="12" fill="var(--ink-muted)">{b}B</text> |
| 82 | + ))} |
| 83 | + <text x={PAD.left + PW / 2} y={H - 2} textAnchor="middle" fontSize="11.5" fill="var(--ink-muted)"> |
| 84 | + active parameters → |
| 85 | + </text> |
| 86 | + {/* the week arrow: base → finetuned, same params */} |
| 87 | + <line x1={x(ghost.params)} y1={y(ghost.score) - 9} x2={x(hero.params)} y2={y(hero.score) + 12} |
| 88 | + stroke="var(--ink-muted)" strokeWidth="1.4" strokeDasharray="4 4" markerEnd="url(#dg-arr)" /> |
| 89 | + <defs> |
| 90 | + <marker id="dg-arr" markerWidth="7" markerHeight="7" refX="5.5" refY="3.5" orient="auto"> |
| 91 | + <path d="M0,0 L7,3.5 L0,7 Z" fill="var(--ink-muted)" /> |
| 92 | + </marker> |
| 93 | + </defs> |
| 94 | + {/* points — provider hues borrowed from the /benchmarks board scatter */} |
| 95 | + {SCATTER.map((p) => { |
| 96 | + const HUE: Record<string, string> = { |
| 97 | + ours: "var(--pO, #a78bfa)", dgbase: "#3978e6", g31: "#3978e6", twin: "#3978e6", |
| 98 | + phi4: "#2774c8", ministral: "#dc5a4f", granite: "#4d6fb8", lfm: "#c04f79", |
| 99 | + }; |
| 100 | + const hue = HUE[p.id] ?? "var(--ink-muted)"; |
| 101 | + const cx = x(p.params); |
| 102 | + const cy = y(p.score); |
| 103 | + if (p.hero) { |
| 104 | + return ( |
| 105 | + <g key={p.id}> |
| 106 | + <circle cx={cx} cy={cy} r="13" fill={hue} opacity=".2" /> |
| 107 | + <circle cx={cx} cy={cy} r="6.5" fill={hue} stroke="var(--surface)" strokeWidth="2" /> |
| 108 | + <text x={cx + 17} y={cy - 8} fontSize="13.5" fontWeight={750} fill="var(--ink)" |
| 109 | + stroke="var(--surface)" strokeWidth={4} paintOrder="stroke"> |
| 110 | + {p.label} |
| 111 | + </text> |
| 112 | + <text x={cx + 17} y={cy + 9} fontSize="11.5" fontWeight={650} fill={hue} |
| 113 | + stroke="var(--surface)" strokeWidth={4} paintOrder="stroke"> |
| 114 | + {p.score}% · up from {SCATTER.find((q) => q.ghost)!.score}% |
| 115 | + </text> |
| 116 | + </g> |
| 117 | + ); |
| 118 | + } |
| 119 | + const anchorRight = p.params > 12; |
| 120 | + return ( |
| 121 | + <g key={p.id}> |
| 122 | + <circle cx={cx} cy={cy} r="4.5" fill={hue} stroke="var(--surface)" strokeWidth="2" /> |
| 123 | + <text x={anchorRight ? cx - 10 : cx + 10} y={cy + 4} fontSize="12" |
| 124 | + textAnchor={anchorRight ? "end" : "start"} fill="var(--ink-muted)" |
| 125 | + stroke="var(--surface)" strokeWidth={4} paintOrder="stroke"> |
| 126 | + {p.label} · {p.score}% |
| 127 | + </text> |
| 128 | + </g> |
| 129 | + ); |
| 130 | + })} |
| 131 | + </svg> |
| 132 | + </div> |
| 133 | + <ChartDataDisclosure> |
| 134 | + <DataTable> |
| 135 | + <thead><tr><th>model</th><th>active params</th><th>OpenUI score</th></tr></thead> |
| 136 | + <tbody> |
| 137 | + {SCATTER.map((p) => ( |
| 138 | + <tr key={p.id}><td>{p.label}</td><td>{p.params}B</td><td>{p.score}%</td></tr> |
| 139 | + ))} |
| 140 | + </tbody> |
| 141 | + </DataTable> |
| 142 | + </ChartDataDisclosure> |
| 143 | + </Chart> |
| 144 | + ); |
| 145 | +} |
| 146 | + |
| 147 | +/* 2 ─ defect anatomy across the three stages -------------------------- */ |
| 148 | + |
| 149 | +export function DgAnatomy() { |
| 150 | + const CLASSES = [ |
| 151 | + { key: "schema" as const, label: "schema errors", slot: 1 as const }, |
| 152 | + { key: "orphans" as const, label: "orphaned sections", slot: 2 as const }, |
| 153 | + { key: "unresolved" as const, label: "undefined names", slot: 3 as const }, |
| 154 | + ]; |
| 155 | + const total = (st: (typeof STAGES)[number]) => st.schema + st.orphans + st.unresolved; |
| 156 | + const maxT = Math.max(...STAGES.map(total)); |
| 157 | + const COLH = 250; |
| 158 | + const reduction = (total(STAGES[0]) / total(STAGES[STAGES.length - 1])).toFixed(1); |
| 159 | + return ( |
| 160 | + <Chart |
| 161 | + title="Where the errors went" |
| 162 | + sub="One bar per stage, split by error class. SFT crushed the grammar but wiring got worse. Self-distillation is what finally moved everything at once." |
| 163 | + legend={CLASSES.map((c) => ({ label: c.label, slot: c.slot }))} |
| 164 | + note={ |
| 165 | + <> |
| 166 | + Defect sites across the same {BOARD_BRIEFS}-brief board, strict parser scoring. The model |
| 167 | + was writing MORE the whole time: {STAGES[0].statements.toLocaleString()} →{" "} |
| 168 | + {STAGES[2].statements.toLocaleString()} statements. |
| 169 | + </> |
| 170 | + } |
| 171 | + > |
| 172 | + <div style={{ display: "flex", gap: 34, alignItems: "flex-end", justifyContent: "center", padding: "10px 8px 4px" }}> |
| 173 | + {STAGES.map((st, i) => { |
| 174 | + const t = total(st); |
| 175 | + const last = i === STAGES.length - 1; |
| 176 | + const rate = ((t / st.statements) * 100).toFixed(1); |
| 177 | + return ( |
| 178 | + <div key={st.id} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 7, flex: "0 1 200px", minWidth: 0 }}> |
| 179 | + <span style={{ fontSize: 15, fontWeight: 700 }}> |
| 180 | + {t} |
| 181 | + {last ? ( |
| 182 | + <span className={slotClass(1)} style={{ fontSize: 12.5, fontWeight: 800, marginLeft: 8 }}> |
| 183 | + {reduction}× fewer |
| 184 | + </span> |
| 185 | + ) : null} |
| 186 | + </span> |
| 187 | + <div style={{ display: "flex", flexDirection: "column", width: "100%", height: (t / maxT) * COLH, borderRadius: 7, overflow: "hidden" }}> |
| 188 | + {CLASSES.map((c) => ( |
| 189 | + <span key={c.key} className={`${s.bar} ${slotClass(c.slot)}`} |
| 190 | + style={{ width: "100%", height: `${(st[c.key] / t) * 100}%`, minHeight: st[c.key] ? 2 : 0, borderRadius: 0 }} /> |
| 191 | + ))} |
| 192 | + </div> |
| 193 | + <span style={{ fontSize: 12.5, fontWeight: 650, textAlign: "center" }}>{st.label}</span> |
| 194 | + <span style={{ fontSize: 11.5, color: "var(--ink-muted)", textAlign: "center", lineHeight: 1.5 }}> |
| 195 | + {st.complete}/{BOARD_BRIEFS} screens complete |
| 196 | + <br /> |
| 197 | + {rate} defects per 100 statements |
| 198 | + </span> |
| 199 | + </div> |
| 200 | + ); |
| 201 | + })} |
| 202 | + </div> |
| 203 | + </Chart> |
| 204 | + ); |
| 205 | +} |
| 206 | + |
| 207 | +/* 3b ─ the self-distillation loop, as a block diagram ------------------ */ |
| 208 | + |
| 209 | +export function DgLoop() { |
| 210 | + const BOXES = [ |
| 211 | + { t: "generate", d: "the model writes a few hundred screens" }, |
| 212 | + { t: "verify", d: "the parser keeps only the perfect ones" }, |
| 213 | + { t: "repair", d: "near-misses fixed by a gated LLM, defects only" }, |
| 214 | + { t: "retrain", d: "the model learns from its own best work" }, |
| 215 | + ]; |
| 216 | + return ( |
| 217 | + <Chart |
| 218 | + title="Reinforcement learning where the compiler is the reward" |
| 219 | + sub="The simplest honest member of the RL family: generate, verify, keep only the wins. We call it self-distillation." |
| 220 | + note={<>Each pass costs about ${ROUND_COST_USD} and two hours on one A100. A repair touches ~2 statements out of 41 on average; the gate rejects anything that rewrites, deletes, or invents.</>} |
| 221 | + > |
| 222 | + <div style={{ position: "relative", padding: "14px 4px 44px" }}> |
| 223 | + <div style={{ display: "flex", alignItems: "stretch", gap: 0, flexWrap: "wrap", justifyContent: "center" }}> |
| 224 | + {BOXES.map((b, i) => ( |
| 225 | + <div key={b.t} style={{ display: "flex", alignItems: "center" }}> |
| 226 | + <div style={{ |
| 227 | + border: "1px solid var(--rule)", borderRadius: 12, padding: "12px 14px", width: 168, |
| 228 | + display: "flex", flexDirection: "column", gap: 4, background: "color-mix(in srgb, var(--rule) 26%, transparent)", |
| 229 | + }}> |
| 230 | + <span className={slotClass(1)} style={{ fontSize: 11, fontWeight: 800, letterSpacing: ".07em", textTransform: "uppercase" }}> |
| 231 | + {i + 1} · {b.t} |
| 232 | + </span> |
| 233 | + <span style={{ fontSize: 12, color: "var(--ink-muted)", lineHeight: 1.45 }}>{b.d}</span> |
| 234 | + </div> |
| 235 | + {i < BOXES.length - 1 ? ( |
| 236 | + <span style={{ padding: "0 9px", color: "var(--ink-muted)", fontSize: 16 }} aria-hidden>→</span> |
| 237 | + ) : null} |
| 238 | + </div> |
| 239 | + ))} |
| 240 | + </div> |
| 241 | + {/* the return path: RETRAIN feeds GENERATE */} |
| 242 | + <svg aria-hidden style={{ position: "absolute", left: "6%", right: "6%", bottom: 2, width: "88%", height: 40, overflow: "visible" }} |
| 243 | + viewBox="0 0 100 30" preserveAspectRatio="none"> |
| 244 | + <defs> |
| 245 | + <marker id="dg-loop-arr" markerWidth="7" markerHeight="7" refX="4.5" refY="3.5" orient="auto"> |
| 246 | + <path d="M0,0 L7,3.5 L0,7 Z" fill="var(--ink-muted)" /> |
| 247 | + </marker> |
| 248 | + </defs> |
| 249 | + <path d="M 92,0 L 92,16 Q 92,23 86,23 L 14,23 Q 8,23 8,16 L 8,4" |
| 250 | + fill="none" stroke="var(--ink-muted)" strokeWidth="1.4" strokeDasharray="4 4" |
| 251 | + vectorEffect="non-scaling-stroke" markerEnd="url(#dg-loop-arr)" /> |
| 252 | + </svg> |
| 253 | + <span style={{ |
| 254 | + position: "absolute", left: "50%", bottom: 4, transform: "translateX(-50%)", |
| 255 | + fontSize: 11.5, color: "var(--ink-muted)", background: "var(--surface)", padding: "0 10px", whiteSpace: "nowrap", |
| 256 | + }}> |
| 257 | + each pass trains the model that writes the next batch |
| 258 | + </span> |
| 259 | + </div> |
| 260 | + </Chart> |
| 261 | + ); |
| 262 | +} |
| 263 | + |
| 264 | +/* 4 ─ the step dial: fewer denoising steps, better screens ------------ */ |
| 265 | + |
| 266 | +export function DgDial() { |
| 267 | + const { holder, w: W } = useWidth(); |
| 268 | + const H = 300; |
| 269 | + const PAD = { left: 44, right: 120, top: 20, bottom: 42 }; |
| 270 | + const PW = W - PAD.left - PAD.right; |
| 271 | + const PH = H - PAD.top - PAD.bottom; |
| 272 | + const stepsAll = [16, 24, 32, 64]; |
| 273 | + const x = (st: number) => PAD.left + (stepsAll.indexOf(st) / (stepsAll.length - 1)) * PW; |
| 274 | + const y = (v: number) => PAD.top + (1 - v / 36) * PH; |
| 275 | + const path = (pts: Array<{ steps: number; complete: number }>) => |
| 276 | + pts.map((p, i) => `${i === 0 ? "M" : "L"} ${x(p.steps)},${y(p.complete)}`).join(" "); |
| 277 | + return ( |
| 278 | + <Chart |
| 279 | + title="Half the denoising steps, better screens" |
| 280 | + sub="Complete screens out of 46 at a fixed step budget. Each round of self-teaching moves the whole curve up and the cliff to the left." |
| 281 | + legend={DIAL.map((d, i) => ({ label: d.label, slot: (i + 1) as 1 | 2 | 3 }))} |
| 282 | + note="At 32 steps the finished model beats its own 64-step score. Below 24, wiring runs out of passes before grammar does." |
| 283 | + > |
| 284 | + <div ref={holder} className={s.svgHolder}> |
| 285 | + <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} role="img" |
| 286 | + aria-label="Line chart: completed screens against denoising steps for round one and round three models."> |
| 287 | + {[0, 12, 24, 36].map((t) => ( |
| 288 | + <g key={t}> |
| 289 | + <line x1={PAD.left} x2={PAD.left + PW} y1={y(t)} y2={y(t)} stroke="var(--rule)" /> |
| 290 | + <text x={PAD.left - 9} y={y(t) + 4} textAnchor="end" fontSize="12" fill="var(--ink-muted)">{t}</text> |
| 291 | + </g> |
| 292 | + ))} |
| 293 | + {stepsAll.map((st) => ( |
| 294 | + <text key={st} x={x(st)} y={H - 18} textAnchor="middle" fontSize="12" fill="var(--ink-muted)">{st}</text> |
| 295 | + ))} |
| 296 | + <text x={PAD.left + PW / 2} y={H - 2} textAnchor="middle" fontSize="11.5" fill="var(--ink-muted)"> |
| 297 | + denoising steps per block → |
| 298 | + </text> |
| 299 | + {DIAL.map((d2, i) => ( |
| 300 | + <g key={d2.id} className={slotClass((i + 1) as 1 | 2 | 3)}> |
| 301 | + <path d={path(d2.points)} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" /> |
| 302 | + {d2.points.map((p) => ( |
| 303 | + <circle key={p.steps} cx={x(p.steps)} cy={y(p.complete)} r="4" fill="currentColor" /> |
| 304 | + ))} |
| 305 | + <text x={x(d2.points[d2.points.length - 1].steps) + 10} |
| 306 | + y={y(d2.points[d2.points.length - 1].complete) + 4} |
| 307 | + fontSize="12" fontWeight={650} fill="currentColor">{d2.short}</text> |
| 308 | + </g> |
| 309 | + ))} |
| 310 | + </svg> |
| 311 | + </div> |
| 312 | + </Chart> |
| 313 | + ); |
| 314 | +} |
| 315 | + |
| 316 | +/* 5 ─ serving speed ---------------------------------------------------- */ |
| 317 | + |
| 318 | +export function DgSpeed() { |
| 319 | + const max = Math.max(...SPEED.map((r) => r.secPerScreen)); |
| 320 | + return ( |
| 321 | + <Chart |
| 322 | + title="Seconds per screen in production" |
| 323 | + sub="Same serving stack for all three: vLLM, FP8, one A100, one request at a time. The last two rows share an identical early-stop sampler." |
| 324 | + note="No serving config changed between the last two rows. The model simply became certain enough that the sampler stops early on its own." |
| 325 | + > |
| 326 | + <div className={s.rows}> |
| 327 | + {SPEED.map((r, i) => ( |
| 328 | + <Row key={r.id} label={r.label} wide tip={r.note}> |
| 329 | + <span className={`${s.bar} ${slotClass(((i % 3) + 1) as 1 | 2 | 3)}`} |
| 330 | + style={{ width: `${(r.secPerScreen / max) * 86}%` }} /> |
| 331 | + <span className={s.value}>{r.secPerScreen}s · {r.toksPerSec} tok/s</span> |
| 332 | + </Row> |
| 333 | + ))} |
| 334 | + </div> |
| 335 | + </Chart> |
| 336 | + ); |
| 337 | +} |
| 338 | + |
| 339 | +/* 6 ─ difficulty bands: where the gain landed -------------------------- */ |
| 340 | + |
| 341 | +export function DgBands() { |
| 342 | + const COLH = 210; |
| 343 | + return ( |
| 344 | + <Chart |
| 345 | + title="The hard screens are where it won" |
| 346 | + sub="Published runs completed per difficulty band, base vs finetuned, same standard prompt. Base collapses as briefs get denser; the finetune keeps going." |
| 347 | + legend={[ |
| 348 | + { label: "base model", slot: 3 }, |
| 349 | + { label: "after SFT + self-distillation", slot: 1 }, |
| 350 | + ]} |
| 351 | + note={ |
| 352 | + <> |
| 353 | + 46 briefs in five bands by complexity, 4 runs each. On the medium band base completed 1 run |
| 354 | + of 40. On the densest band the finetune reaches {B5_OWN_PROMPT}/32 with the prompt it was |
| 355 | + trained with. |
| 356 | + </> |
| 357 | + } |
| 358 | + > |
| 359 | + <div style={{ display: "flex", gap: 26, alignItems: "flex-end", justifyContent: "center", padding: "10px 8px 4px" }}> |
| 360 | + {BANDS.map((b) => ( |
| 361 | + <div key={b.id} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 7, flex: "0 1 130px", minWidth: 0 }}> |
| 362 | + <div style={{ display: "flex", gap: 6, alignItems: "flex-end", width: "100%", justifyContent: "center", height: COLH }}> |
| 363 | + {([["base", 3], ["ours", 1]] as const).map(([k, slot]) => ( |
| 364 | + <div key={k} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 4, width: 44 }}> |
| 365 | + <span style={{ fontSize: 12, fontWeight: 700, color: k === "base" ? "var(--ink-muted)" : undefined }} |
| 366 | + className={k === "ours" ? slotClass(1) : undefined}> |
| 367 | + {b[k]} |
| 368 | + </span> |
| 369 | + <span className={`${s.bar} ${slotClass(slot)}`} |
| 370 | + style={{ width: "100%", height: Math.max(3, (b[k] / b.runs) * (COLH - 26)), borderRadius: 5 }} /> |
| 371 | + </div> |
| 372 | + ))} |
| 373 | + </div> |
| 374 | + <span style={{ fontSize: 12, fontWeight: 650 }}>{b.label}</span> |
| 375 | + <span style={{ fontSize: 11, color: "var(--ink-muted)" }}>of {b.runs} runs</span> |
| 376 | + </div> |
| 377 | + ))} |
| 378 | + </div> |
| 379 | + </Chart> |
| 380 | + ); |
| 381 | +} |
0 commit comments