Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
| <title>25. SFT / DPO / RLHF / GRPO β LM Playground</title> | |
| <link rel="stylesheet" href="/platform/platform.css"/> | |
| <script type="module" src="/platform/platform.js"></script> | |
| <style> | |
| :root{ | |
| --bg:#0f1117; --panel:#181b24; --panel-2:#1f2330; --border:#2a2f3d; | |
| --text:#e6e8ee; --muted:#9aa3b2; --accent:#7c8cff; --accent-2:#5be0c0; | |
| --good:#5be08a; --warn:#ffc06b; --bad:#ff9090; | |
| --ref:#9aa3b2; --sft:#ffc06b; --dpo:#7c8cff; --grpo:#5be0c0; | |
| --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{margin:0;padding:0 20px 80px;background:radial-gradient(1200px 600px at 50% -10%,#1a1f2e 0%,var(--bg) 55%);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.5;-webkit-font-smoothing:antialiased} | |
| header{max-width:980px;margin:0 auto;padding:34px 0 0} | |
| .header-inner{display:flex;justify-content:space-between;align-items:flex-start;padding-bottom:12px} | |
| h1{font-size:30px;margin:0 0 6px;letter-spacing:-.5px} | |
| .sub{color:var(--muted);max-width:780px;margin:0;font-size:14.5px;line-height:1.6} | |
| .sub b{color:var(--text);font-weight:600} | |
| .page-tabs{display:flex;border-bottom:1px solid var(--border)} | |
| .page-tab{background:none;border:none;color:var(--muted);padding:11px 22px 10px;font-size:14px;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:.12s} | |
| .page-tab:hover{color:var(--text)}.page-tab.active{color:var(--accent);border-bottom-color:var(--accent)} | |
| .tab-panel{display:none}.tab-panel.active{display:block} | |
| .guide{max-width:980px;margin:0 auto;padding:30px 0 80px;display:grid;grid-template-columns:1fr 220px;gap:0 48px} | |
| .guide-toc{grid-column:2;grid-row:1/20;position:sticky;top:64px;align-self:start;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:4px} | |
| .toc-label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);font-weight:700;margin-bottom:6px} | |
| .guide-toc a{color:var(--muted);text-decoration:none;font-size:12.5px;padding:5px 8px;border-radius:6px;transition:.1s} | |
| .guide-toc a:hover{color:var(--text);background:var(--panel-2)} | |
| .chapter{grid-column:1;padding:0 0 48px;border-bottom:1px solid var(--border);margin-bottom:48px} | |
| .chapter:last-of-type{border-bottom:none} | |
| .chapter h2{font-size:22px;margin:0 0 18px;display:flex;align-items:center;gap:12px;letter-spacing:-.3px} | |
| .ch-num{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:99px;background:var(--accent-2);color:#0b0d14;font-size:14px;font-weight:800;flex-shrink:0} | |
| .chapter h3{font-size:15px;margin:24px 0 8px;color:var(--accent-2);font-weight:700} | |
| .chapter p{margin:0 0 14px;line-height:1.7;color:#ced3de;font-size:15px} | |
| .chapter strong{color:var(--text)}.chapter em{font-style:italic} | |
| code{background:var(--panel-2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-family:var(--mono);font-size:12.5px;color:var(--accent-2)} | |
| .callout{border-radius:10px;padding:14px 16px;margin:20px 0;font-size:14px;line-height:1.65;color:#ced3de} | |
| .callout strong{display:block;margin-bottom:5px;font-size:12px;text-transform:uppercase;letter-spacing:.6px} | |
| .callout.insight{background:#7c8cff18;border-left:3px solid var(--accent)}.callout.insight strong{color:var(--accent)} | |
| .callout.warn{background:#ffc06b18;border-left:3px solid var(--warn)}.callout.warn strong{color:var(--warn)} | |
| .try-it{display:inline-block;margin-top:10px;padding:10px 18px;border-radius:9px;background:var(--panel-2);border:1px solid var(--border);color:var(--accent);font-size:13.5px;font-weight:600;cursor:pointer;transition:.14s;font-family:inherit} | |
| .try-it:hover{background:#7c8cff22;border-color:var(--accent)} | |
| .try-it.large{font-size:15px;padding:14px 28px;background:var(--accent-2);color:#0b0d14;border-color:var(--accent-2)} | |
| .try-it.large:hover{filter:brightness(1.08)} | |
| .guide-end{text-align:center;padding:18px 0 0}.guide-end p{color:var(--muted);margin-bottom:14px} | |
| .panel-guide-item{display:flex;gap:12px;align-items:baseline;padding:9px 0;border-bottom:1px dashed var(--border)} | |
| .panel-guide-item:last-child{border-bottom:none} | |
| .pgi-label{font-size:13px;font-weight:800;color:var(--accent-2);min-width:20px;font-family:var(--mono)} | |
| .panel-guide-item p{margin:0;font-size:13.5px;color:#ced3de;line-height:1.55} | |
| .wrap{max-width:980px;margin:20px auto 0} | |
| .panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px} | |
| .panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px} | |
| .panel-label{font-size:13px;font-weight:700;color:var(--text)}.panel-note{color:var(--muted);font-size:12px} | |
| .ctrl-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin:4px 0 6px} | |
| .ctrl{display:flex;flex-direction:column;gap:6px} | |
| .ctrl .lab{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:600} | |
| .ctrl .lab b{color:var(--text);font-family:var(--mono)} | |
| input[type=range]{accent-color:var(--accent);width:140px} | |
| .btn{border:1px solid var(--border);background:var(--panel-2);color:var(--text);font-size:13px;font-weight:600;padding:9px 16px;border-radius:9px;cursor:pointer;font-family:inherit} | |
| .btn:hover{border-color:var(--accent)}.btn.primary{background:var(--accent-2);color:#0b0d14;border-color:var(--accent-2)} | |
| svg{display:block;width:100%;height:auto;overflow:visible} | |
| .polrow{display:grid;grid-template-columns:repeat(4,1fr);gap:12px} | |
| .polcard{background:var(--panel-2);border:1px solid var(--border);border-radius:11px;padding:12px} | |
| .polcard h4{margin:0 0 6px;font-size:12.5px;display:flex;align-items:center;gap:6px} | |
| .polcard .dot{width:9px;height:9px;border-radius:50%} | |
| .polcard .note{font-size:10.5px;color:var(--muted);margin-top:6px;line-height:1.4} | |
| .cmptable{width:100%;border-collapse:collapse;font-size:12px} | |
| .cmptable th,.cmptable td{padding:7px 8px;text-align:left;border-bottom:1px solid var(--border);vertical-align:top} | |
| .cmptable th{color:var(--muted);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.4px} | |
| .cmptable .m{font-weight:700} | |
| .hint{color:var(--muted);font-size:12.5px;margin-top:10px;line-height:1.5} | |
| .verdict{font-size:12.5px;margin-top:10px;padding:9px 12px;border-radius:7px;font-weight:600;line-height:1.5;background:var(--panel-2);border:1px solid var(--border);color:#ced3de} | |
| .fig{margin:22px 0;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 16px 12px} | |
| .fig svg{display:block;width:100%;height:auto;overflow:visible} | |
| .fig figcaption{margin-top:10px;font-size:12px;color:var(--muted);text-align:center;line-height:1.55} | |
| footer{max-width:980px;margin:30px auto 0;color:var(--muted);font-size:12px;text-align:center;line-height:1.6} | |
| @media(max-width:760px){.guide{grid-template-columns:1fr}.guide-toc{grid-column:1;grid-row:auto;position:static;display:grid;grid-template-columns:1fr 1fr}.toc-label{grid-column:1/-1}.polrow{grid-template-columns:1fr 1fr}} | |
| </style> | |
| </head> | |
| <body data-chapter="alignment"> | |
| <header> | |
| <div class="header-inner"> | |
| <div> | |
| <h1>Alignment: SFT, RLHF, DPO & GRPO</h1> | |
| <p class="sub"> | |
| Pretraining gives a model knowledge and the knack for continuing text. It does not give it | |
| manners β the instinct to follow instructions, be helpful, refuse the harmful, and prefer the | |
| better answer over the merely plausible one. Post-training installs all of that, and there are | |
| a few ways to do it: imitate good answers, or learn from which answers people <b>preferred</b>. | |
| Same model, same preferences, surprisingly different results. | |
| </p> | |
| </div> | |
| </div> | |
| <nav class="page-tabs" role="tablist"> | |
| <button class="page-tab active" data-tab="guide">β Guide</button> | |
| <button class="page-tab" data-tab="playground">β‘ Playground</button> | |
| </nav> | |
| </header> | |
| <!-- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ GUIDE --> | |
| <section id="guide-tab" class="tab-panel active"> | |
| <article class="guide"> | |
| <nav class="guide-toc"> | |
| <span class="toc-label">Contents</span> | |
| <a href="#ch1">1 Β· Knowledge isn't manners</a> | |
| <a href="#ch2">2 Β· SFT: imitate</a> | |
| <a href="#ch3">3 Β· RLHF: reward & RL</a> | |
| <a href="#ch4">4 Β· DPO: skip the middle</a> | |
| <a href="#ch5">5 Β· GRPO: group-relative</a> | |
| <a href="#ch6">6 Β· Reading the playground</a> | |
| </nav> | |
| <!-- 1 --> | |
| <section class="chapter" id="ch1"> | |
| <h2><span class="ch-num">1</span> Knowledge isn't manners</h2> | |
| <p> | |
| A freshly pretrained model is a strange thing to talk to. It knows an enormous amount, but its | |
| only skill is continuing text, so it'll happily complete your question with more questions, | |
| ramble, or echo the worst of its training data. It has no notion that it should be helpful, that | |
| it should follow an instruction, or that one answer might be better than another. | |
| </p> | |
| <figure class="fig"><svg viewBox="0 0 700 200" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,Segoe UI,Roboto,sans-serif"><defs><marker id="aral" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--muted)"/></marker></defs> | |
| <rect x="30" y="74" width="96" height="46" rx="9" fill="var(--panel-2)" stroke="var(--border)"/> | |
| <text x="78" y="94" text-anchor="middle" fill="var(--text)" font-size="11.5" font-weight="700">pretrained</text> | |
| <text x="78" y="109" text-anchor="middle" fill="var(--muted)" font-size="9">knows language</text> | |
| <line x1="126" y1="97" x2="166" y2="97" stroke="var(--border)" marker-end="url(#aral)"/> | |
| <rect x="170" y="74" width="96" height="46" rx="9" fill="var(--panel-2)" stroke="var(--accent-2)"/> | |
| <text x="218" y="94" text-anchor="middle" fill="var(--text)" font-size="11.5" font-weight="700">SFT</text> | |
| <text x="218" y="109" text-anchor="middle" fill="var(--muted)" font-size="9">follows instructions</text> | |
| <line x1="266" y1="97" x2="306" y2="97" stroke="var(--border)" marker-end="url(#aral)"/> | |
| <rect x="310" y="44" width="150" height="34" rx="7" fill="var(--good)" opacity="0.18" stroke="var(--good)"/> | |
| <text x="385" y="65" text-anchor="middle" fill="var(--good)" font-size="10.5" font-weight="700">β preferred answer</text> | |
| <rect x="310" y="116" width="150" height="34" rx="7" fill="var(--bad)" opacity="0.15" stroke="var(--bad)"/> | |
| <text x="385" y="137" text-anchor="middle" fill="var(--bad)" font-size="10.5" font-weight="700">β rejected answer</text> | |
| <line x1="266" y1="90" x2="306" y2="64" stroke="var(--border)" marker-end="url(#aral)"/> | |
| <line x1="266" y1="104" x2="306" y2="130" stroke="var(--border)" marker-end="url(#aral)"/> | |
| <line x1="460" y1="61" x2="520" y2="90" stroke="var(--good)" marker-end="url(#aral)"/> | |
| <line x1="460" y1="133" x2="520" y2="104" stroke="var(--bad)" stroke-dasharray="3 3" marker-end="url(#aral)"/> | |
| <rect x="524" y="74" width="146" height="46" rx="9" fill="var(--accent)" opacity="0.2" stroke="var(--accent)"/> | |
| <text x="597" y="94" text-anchor="middle" fill="var(--text)" font-size="11.5" font-weight="700">aligned policy</text> | |
| <text x="597" y="109" text-anchor="middle" fill="var(--muted)" font-size="9">DPO Β· RLHF Β· GRPO</text> | |
| </svg><figcaption>Alignment turns a raw next-token predictor into a helpful assistant in stages. Supervised fine-tuning teaches it to follow instructions; preference methods (DPO, RLHF, GRPO) then push it toward answers people prefer and away from ones they reject β learning from comparisons, not just single correct labels.</figcaption></figure> | |
| <p> | |
| Post-training β alignment β is the stage that turns that raw model into something you'd want to | |
| use. The knowledge is already there; what's missing is a sense of what a <em>good response</em> | |
| looks like. And the cleanest signal for "good" turns out not to be a perfect example, but a | |
| comparison: this answer is better than that one. Most of modern alignment is about learning from | |
| those comparisons. | |
| </p> | |
| <div class="callout insight"> | |
| <strong>Alignment shapes behaviour, not knowledge.</strong> | |
| Pretraining decides what the model knows; post-training decides how it acts on it. The same | |
| weights become a helpful assistant or stay an unruly text-completer depending entirely on this | |
| stage. | |
| </div> | |
| <button class="try-it" data-action="open">βΆ Align the same policy three ways</button> | |
| </section> | |
| <!-- 2 --> | |
| <section class="chapter" id="ch2"> | |
| <h2><span class="ch-num">2</span> SFT: imitate the good answers</h2> | |
| <p> | |
| The first and simplest step is <strong>supervised fine-tuning</strong>. You collect a set of | |
| high-quality <em>(instruction, ideal response)</em> pairs β written by humans or distilled from a | |
| stronger model β and train the model to reproduce them, exactly the next-token objective from | |
| Chapter 8, now on curated data. This alone is transformative: it teaches the model the | |
| assistant format and basic helpfulness, and turns the text-completer into something that answers. | |
| </p> | |
| <p> | |
| But SFT can only imitate. It learns from <em>positive</em> examples β "produce this" β and never | |
| from "this is worse than that." If your demonstration is merely good and a better answer exists, | |
| SFT has no way to find it; it concentrates the model's probability on the example it was shown, | |
| whatever its quality. To go further, you need a signal about <em>relative</em> quality. | |
| </p> | |
| <div class="callout warn"> | |
| <strong>SFT imitates; it can't compare.</strong> | |
| Trained only on demonstrations, the model's ceiling is the quality of those demonstrations. It | |
| will happily learn a mediocre answer if that's what it was shown, with no mechanism to prefer | |
| something better. | |
| </div> | |
| </section> | |
| <!-- 3 --> | |
| <section class="chapter" id="ch3"> | |
| <h2><span class="ch-num">3</span> RLHF: a reward model, then RL</h2> | |
| <p> | |
| <strong>Reinforcement learning from human feedback</strong> brings in the comparison signal. You | |
| show people two responses and ask which they prefer; from thousands of these pairs you train a | |
| <em>reward model</em> that scores any response with a number predicting human preference. Then you | |
| use reinforcement learning β classically PPO β to update the policy so it produces responses the | |
| reward model rates highly, with a leash (a KL penalty) keeping it from drifting too far from the | |
| SFT model. | |
| </p> | |
| <p> | |
| This is powerful: the model can now <em>exceed</em> its demonstrations, discovering high-reward | |
| responses no human wrote, because it's optimizing a score rather than copying examples. It's also | |
| a lot of moving parts β a separate reward model, an RL loop that's famously finicky to stabilize, | |
| and the risk of the policy learning to game the reward model rather than genuinely improve. | |
| </p> | |
| <div class="callout insight"> | |
| <strong>RLHF optimizes a score, so it can beat the demos.</strong> | |
| By chasing a reward instead of imitating, the policy explores beyond what it was shown. The cost | |
| is complexity β a reward model plus a temperamental RL loop β and the ever-present danger of | |
| reward hacking. | |
| </div> | |
| </section> | |
| <!-- 4 --> | |
| <section class="chapter" id="ch4"> | |
| <h2><span class="ch-num">4</span> DPO: skip the reward model</h2> | |
| <p> | |
| <strong>Direct preference optimization</strong> is the insight that you don't actually need the | |
| reward model or the RL loop. The math of RLHF can be rearranged so that the optimal policy is | |
| expressed directly in terms of the preference data β which means you can train the policy on the | |
| preference pairs with a single, stable supervised-style loss. No reward model, no PPO, no | |
| sampling loop. | |
| </p> | |
| <p> | |
| The DPO loss simply pushes up the probability of each <em>chosen</em> response relative to its | |
| <em>rejected</em> partner, anchored to the reference model by a strength knob. It's far easier to | |
| implement and to stabilize than RLHF, which is why it became the default for preference tuning in | |
| the open-source world. The trade is that it learns only from the comparisons you give it β it | |
| can't explore for new high-reward responses the way an RL loop can. | |
| </p> | |
| <div class="callout insight"> | |
| <strong>DPO is RLHF's objective without the machinery.</strong> | |
| Same goal β make preferred answers more likely β reached with one stable loss instead of a reward | |
| model and an RL loop. Simpler and steadier, at the cost of the RL family's ability to explore. | |
| </div> | |
| <button class="try-it" data-action="dpo">βΆ Compare DPO and RLHF on the same preferences</button> | |
| </section> | |
| <!-- 5 --> | |
| <section class="chapter" id="ch5"> | |
| <h2><span class="ch-num">5</span> GRPO: group-relative, for reasoning</h2> | |
| <p> | |
| <strong>Group relative policy optimization</strong>, popularized by the DeepSeek reasoning models, | |
| is an RL method tuned for tasks where you can <em>check</em> the answer β math, code, anything | |
| with a verifier. For each prompt it samples a whole <em>group</em> of responses, scores them, and | |
| computes each one's advantage as its reward minus the group's average. Responses better than their | |
| peers are reinforced; worse-than-average ones are pushed down. | |
| </p> | |
| <p> | |
| The clever part is that the group's own average serves as the baseline, so GRPO needs no separate | |
| value model β a big simplification over PPO. Paired with a verifier that gives a clean reward | |
| (the code passes its tests, or it doesn't), it lets a model bootstrap strong reasoning by | |
| practising against itself, which is much of how the recent reasoning models were trained. | |
| </p> | |
| <div class="callout insight"> | |
| <strong>Score a group, reinforce the above-average.</strong> | |
| Using the group mean as the baseline removes the value model and makes RL cheap and stable. With a | |
| verifiable reward, GRPO turns "generate and check" into a self-improvement loop β the engine of | |
| modern reasoning training. | |
| </div> | |
| <button class="try-it" data-action="grpo">βΆ Watch GRPO find the best response</button> | |
| </section> | |
| <!-- 6 --> | |
| <section class="chapter" id="ch6"> | |
| <h2><span class="ch-num">6</span> Reading the playground</h2> | |
| <p> | |
| The same starting policy β a distribution over six candidate responses, each with a hidden true | |
| reward β is post-trained three ways with the <em>real</em> update rules: SFT's imitation | |
| gradient, the DPO preference loss, and GRPO's group-relative policy gradient. Watch where they | |
| end up. | |
| </p> | |
| <div class="panel-guide-item"><span class="pgi-label">β¦</span> | |
| <p>Four distributions: the reference policy and the result of SFT, DPO, and GRPO β over the same | |
| six responses, with their true rewards shown beneath.</p></div> | |
| <div class="panel-guide-item"><span class="pgi-label">βΆ</span> | |
| <p>Step through training and watch each method's probability mass move β SFT toward its | |
| demonstration, DPO toward the preferred pairs, GRPO toward the highest reward.</p></div> | |
| <div class="panel-guide-item"><span class="pgi-label">β</span> | |
| <p>The comparison table β what each method needs, and where they diverge.</p></div> | |
| <div class="guide-end"> | |
| <p>The reading is the setup. The playground is the point.</p> | |
| <button class="try-it large" data-action="open">βΆ Open the Playground</button> | |
| </div> | |
| </section> | |
| </article> | |
| </section> | |
| <!-- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ PLAYGROUND --> | |
| <section id="playground-tab" class="tab-panel"> | |
| <div class="wrap"> | |
| <div class="panel"> | |
| <div class="panel-head"><span class="panel-label">Post-train the same policy</span> | |
| <span class="panel-note">6 candidate responses Β· hidden true rewards</span></div> | |
| <div class="ctrl-row"> | |
| <button class="btn primary" id="stepBtn">βΆ Train 20 steps</button> | |
| <button class="btn" id="resetBtn">βΊ Reset</button> | |
| <div class="ctrl"><span class="lab">Reference anchor Ξ²: <b id="betaVal">0.3</b></span> | |
| <input type="range" id="beta" min="0" max="100" value="30"></div> | |
| <div class="ctrl"><span class="lab">Steps trained: <b id="stepsVal">0</b></span></div> | |
| </div> | |
| </div> | |
| <div class="panel" id="panelPolicies"> | |
| <div class="panel-head"><span class="panel-label">Step 1 Β· where each method takes the policy</span> | |
| <span class="panel-note">probability over the six responses</span></div> | |
| <div class="polrow"> | |
| <div class="polcard"><h4><span class="dot" style="background:var(--ref)"></span>Reference (pretrained)</h4><svg id="polRef" viewBox="0 0 200 130"></svg><div class="note">where we start β a default leaning on the safe response 0</div></div> | |
| <div class="polcard"><h4><span class="dot" style="background:var(--sft)"></span>SFT</h4><svg id="polSft" viewBox="0 0 200 130"></svg><div class="note" id="noteSft"></div></div> | |
| <div class="polcard"><h4><span class="dot" style="background:var(--dpo)"></span>DPO</h4><svg id="polDpo" viewBox="0 0 200 130"></svg><div class="note" id="noteDpo"></div></div> | |
| <div class="polcard"><h4><span class="dot" style="background:var(--grpo)"></span>GRPO</h4><svg id="polGrpo" viewBox="0 0 200 130"></svg><div class="note" id="noteGrpo"></div></div> | |
| </div> | |
| <div class="panel-head" style="margin-top:14px;margin-bottom:6px"><span class="panel-label" style="font-size:12px;color:var(--muted)">true reward of each response (hidden from SFT & DPO)</span></div> | |
| <svg id="rewardBar" viewBox="0 0 900 70"></svg> | |
| <div class="verdict" id="alignVerdict"></div> | |
| </div> | |
| <div class="panel" id="panelTable"> | |
| <div class="panel-head"><span class="panel-label">Step 2 Β· what each method needs, and learns</span></div> | |
| <table class="cmptable"> | |
| <thead><tr><th>method</th><th>signal it uses</th><th>extra machinery</th><th>can exceed demos?</th><th>stability</th></tr></thead> | |
| <tbody> | |
| <tr><td class="m" style="color:var(--sft)">SFT</td><td>good examples (positives only)</td><td>none</td><td>no β imitates</td><td>very stable</td></tr> | |
| <tr><td class="m" style="color:var(--accent)">RLHF</td><td>human preference pairs</td><td>reward model + PPO loop</td><td>yes</td><td>finicky</td></tr> | |
| <tr><td class="m" style="color:var(--dpo)">DPO</td><td>preference pairs</td><td>none (one loss)</td><td>limited to the pairs</td><td>stable</td></tr> | |
| <tr><td class="m" style="color:var(--grpo)">GRPO</td><td>a verifier / reward, per group</td><td>RL loop, no value model</td><td>yes β explores</td><td>stable-ish</td></tr> | |
| </tbody> | |
| </table> | |
| <p class="hint">SFT imitates a demonstration; DPO leans on which answers were preferred; the RL | |
| family (RLHF, GRPO) optimizes a reward and can discover the best response on its own.</p> | |
| </div> | |
| <footer> | |
| A toy over six responses with real update rules β SFT's imitation gradient, the DPO preference | |
| loss (chosen vs rejected, anchored to the reference by Ξ²), and GRPO's group-relative policy | |
| gradient with a KL leash. Rewards are illustrative; the dynamics are the actual ones. | |
| </footer> | |
| </div> | |
| </section> | |
| <script> | |
| ; | |
| /* ββ tabs / toc / try-it ββ */ | |
| function switchTab(name){document.querySelectorAll(".page-tab").forEach(b=>{const on=b.dataset.tab===name;b.classList.toggle("active",on);b.setAttribute("aria-selected",on);});document.querySelectorAll(".tab-panel").forEach(p=>p.classList.toggle("active",p.id===name+"-tab"));} | |
| document.querySelectorAll(".page-tab").forEach(b=>b.addEventListener("click",()=>switchTab(b.dataset.tab))); | |
| document.querySelectorAll(".guide-toc a").forEach(a=>a.addEventListener("click",e=>{e.preventDefault();document.querySelector(a.getAttribute("href"))?.scrollIntoView({behavior:"smooth",block:"start"});})); | |
| document.querySelectorAll(".try-it[data-action]").forEach(b=>b.addEventListener("click",()=>{switchTab("playground");window.scrollTo({top:0,behavior:"smooth"});})); | |
| /* ββββββββββββββββ ENGINE β real policy updates over K responses ββββββββββββββββ */ | |
| const K=6; | |
| // true rewards (hidden): response 3 is best, 0 is the "safe" default, 1 is the SFT demo | |
| const REWARD=[0.35,0.55,0.70,0.95,0.45,0.25]; | |
| const DEMO=1; // SFT demonstration (a decent but not best answer) | |
| const PREFS=[[3,0],[2,4],[1,5],[3,1],[2,0],[3,4]]; // (chosen, rejected) preference pairs | |
| // reference logits: leaning on response 0 (safe default) | |
| const THETA_REF=[1.2,0.3,0.1,0.0,0.2,0.4]; | |
| function softmax(th){let m=Math.max(...th);let Z=0;const e=th.map(x=>{const v=Math.exp(x-m);Z+=v;return v;});return e.map(x=>x/Z);} | |
| function sigmoid(x){return 1/(1+Math.exp(-x));} | |
| // SFT: maximize log Ο(demo) β grad ΞΈ = onehot(demo) - Ο | |
| function stepSFT(th,lr){const p=softmax(th);return th.map((v,i)=>v+lr*(((i===DEMO)?1:0)-p[i]));} | |
| // DPO: for each pref (c,j), margin = Ξ²((ΞΈc-ΞΈref_c)-(ΞΈj-ΞΈref_j)); push ΞΈc up, ΞΈj down by lrΒ·Ξ²Β·(1-Ο(margin)) | |
| function stepDPO(th,lr,beta){const nt=th.slice();for(const [c,j] of PREFS){const margin=beta*((th[c]-THETA_REF[c])-(th[j]-THETA_REF[j]));const g=lr*beta*(1-sigmoid(margin));nt[c]+=g;nt[j]-=g;}return nt;} | |
| // GRPO: policy gradient on reward with group-mean baseline, KL leash to reference. | |
| // ΞΈi += lr*( Ο_i*(r_i - E_Ο[r]) - Ξ²*Ο_i*((ΞΈi-ΞΈref_i) - mean) ) | |
| function stepGRPO(th,lr,beta){const p=softmax(th);const rbar=p.reduce((s,pi,i)=>s+pi*REWARD[i],0); | |
| const dev=th.map((v,i)=>v-THETA_REF[i]);const dbar=p.reduce((s,pi,i)=>s+pi*dev[i],0); | |
| return th.map((v,i)=>v+lr*(p[i]*(REWARD[i]-rbar) - beta*p[i]*(dev[i]-dbar))*6);} | |
| /* ββββββββββββββββ UI ββββββββββββββββ */ | |
| const $=id=>document.getElementById(id); | |
| const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim(); | |
| const S={steps:0,beta:0.3,sft:THETA_REF.slice(),dpo:THETA_REF.slice(),grpo:THETA_REF.slice()}; | |
| function reset(){S.steps=0;S.sft=THETA_REF.slice();S.dpo=THETA_REF.slice();S.grpo=THETA_REF.slice();render();} | |
| function train(n){for(let i=0;i<n;i++){S.sft=stepSFT(S.sft,0.3);S.dpo=stepDPO(S.dpo,0.25,S.beta>0?S.beta:0.05);S.grpo=stepGRPO(S.grpo,0.3,S.beta);}S.steps+=n;render();} | |
| function drawPol(svgId,th,col){const p=softmax(th),W=200,H=130,padB=20,padT=10,bw=(W-12)/K,mx=Math.max(...p,0.5);let s=""; | |
| for(let i=0;i<K;i++){const h=(p[i]/mx)*(H-padT-padB),x=6+i*bw;s+='<rect x="'+(x+2)+'" y="'+(H-padB-h)+'" width="'+(bw-4)+'" height="'+h.toFixed(1)+'" rx="2" fill="'+col+'"/>'; | |
| s+='<text x="'+(x+bw/2)+'" y="'+(H-padB+13)+'" font-size="8.5" text-anchor="middle" fill="'+css('--muted')+'">'+i+'</text>'; | |
| if(p[i]>0.12)s+='<text x="'+(x+bw/2)+'" y="'+(H-padB-h-3)+'" font-size="8" text-anchor="middle" fill="'+css('--muted')+'">'+Math.round(p[i]*100)+'</text>';} | |
| $(svgId).innerHTML=s;} | |
| function argmax(a){let bi=0;for(let i=1;i<a.length;i++)if(a[i]>a[bi])bi=i;return bi;} | |
| function render(){ | |
| $('stepsVal').textContent=S.steps;$('betaVal').textContent=S.beta.toFixed(2); | |
| drawPol('polRef',THETA_REF,css('--ref'));drawPol('polSft',S.sft,css('--sft'));drawPol('polDpo',S.dpo,css('--dpo'));drawPol('polGrpo',S.grpo,css('--grpo')); | |
| // reward bars | |
| const W=900,H=70,bw=(W-12)/K,mx=Math.max(...REWARD);let s=""; | |
| for(let i=0;i<K;i++){const h=(REWARD[i]/mx)*40,x=6+i*bw,best=i===argmax(REWARD); | |
| s+='<rect x="'+(x+bw*0.3)+'" y="'+(50-h)+'" width="'+(bw*0.4)+'" height="'+h.toFixed(1)+'" rx="2" fill="'+(best?css('--good'):css('--muted'))+'"/>'; | |
| s+='<text x="'+(x+bw/2)+'" y="'+62+'" font-size="9" text-anchor="middle" fill="'+(best?css('--good'):css('--muted'))+'">r'+i+'='+REWARD[i].toFixed(2)+(best?' β ':'')+'</text>';} | |
| $('rewardBar').innerHTML=s; | |
| const ps=softmax(S.sft),pd=softmax(S.dpo),pg=softmax(S.grpo); | |
| $('noteSft').textContent='mass β response '+argmax(ps)+' (its demonstration), reward '+REWARD[argmax(ps)].toFixed(2); | |
| $('noteDpo').textContent='mass β response '+argmax(pd)+' (most-preferred), reward '+REWARD[argmax(pd)].toFixed(2); | |
| $('noteGrpo').textContent='mass β response '+argmax(pg)+' (highest reward), reward '+REWARD[argmax(pg)].toFixed(2); | |
| const v=$('alignVerdict'); | |
| if(S.steps<10){v.textContent='Train a few steps to see the three methods pull the same policy in different directions.';} | |
| else v.innerHTML='After '+S.steps+' steps they diverge: <b style="color:var(--sft)">SFT</b> locked onto its demonstration (response '+argmax(ps)+', reward '+REWARD[argmax(ps)].toFixed(2)+'); <b style="color:var(--dpo)">DPO</b> moved to the preferred answer (response '+argmax(pd)+'); <b style="color:var(--grpo)">GRPO</b> optimized the reward and found the best response '+argmax(pg)+' ('+REWARD[argmax(pg)].toFixed(2)+') β even though no one demonstrated it.'; | |
| } | |
| /* ββ events ββ */ | |
| $('stepBtn').addEventListener('click',()=>train(20)); | |
| $('resetBtn').addEventListener('click',reset); | |
| $('beta').addEventListener('input',e=>{S.beta=+e.target.value/100;reset();}); | |
| reset(); | |
| </script> | |
| </body> | |
| </html> | |