multi-harness-rl-slides / source /src /embeds /d3-rollout-graph.html
sergiopaniego's picture
sergiopaniego HF Staff
Publish the deck source under source/ so the next deck can start from it
3bc016b verified
Raw History Blame Contribute Delete
11.9 kB
<div class="rollout-graph" style="width:100%;margin:14px 0;"></div>
<style>
.rollout-graph { position: relative; }
.rollout-graph svg { display:block; width:100%; height:auto; overflow:visible; }
.rollout-graph .rg-pick { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:12px; }
.rollout-graph .rg-btn { appearance:none; cursor:pointer; font:inherit; font-size:12px; line-height:1;
padding:7px 12px; border-radius:999px; border:1px solid var(--border-color);
background:transparent; color:var(--muted-color); transition:color .15s, border-color .15s; }
.rollout-graph .rg-btn:hover { color:var(--text-color); }
.rollout-graph .rg-btn[aria-pressed="true"] { color:var(--text-color); border-color:var(--text-color); font-weight:600; }
.rollout-graph .rg-btn:focus-visible { outline:2px solid var(--text-color); outline-offset:2px; }
.rollout-graph .rg-score { display:flex; flex-wrap:wrap; gap:8px 22px; margin-top:12px; }
.rollout-graph .rg-stat { display:flex; flex-direction:column; gap:2px; }
.rollout-graph .rg-stat b { font-size:17px; font-weight:700; color:var(--text-color);
font-variant-numeric:tabular-nums; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
.rollout-graph .rg-stat span { font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--muted-color); }
.rollout-graph .rg-lg { display:flex; flex-wrap:wrap; gap:6px 16px; margin-top:12px; font-size:12px; color:var(--muted-color); }
.rollout-graph .rg-lg-t { width:100%; font-size:11px; letter-spacing:.08em; text-transform:uppercase; opacity:.75; }
.rollout-graph .rg-k { display:inline-flex; align-items:center; gap:7px; }
.rollout-graph .rg-sw { width:11px; height:11px; }
.rollout-graph .rg-nt { margin-top:10px; font-size:12px; line-height:1.55; color:var(--muted-color); }
</style>
<script>
(() => {
const C = 'rollout-graph';
const boot = () => {
const nodes0 = [...document.querySelectorAll('.'+C)].filter(e => e.dataset.mounted !== 'true');
const r = nodes0[nodes0.length-1]; if (!r) return; r.dataset.mounted = 'true';
const V = (k,f) => (getComputedStyle(r).getPropertyValue(k).trim() || f);
const P = (i) => {
if (window.ColorPalettes && window.ColorPalettes.categorical) {
try { return window.ColorPalettes.categorical(4)[i%4]; } catch(e) {}
}
return ['rgb(78,165,183)','rgb(146,176,234)','rgb(206,192,250)','rgb(232,137,171)'][i%4];
};
const esc = (t) => String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
// A trace is a DAG of unique messages, each linked to its predecessor. Every
// root-to-leaf path is a branch, and a branch is what a trainer can treat as
// one contiguous sample. Two things create branches here: compaction, which
// reattaches the summarised continuation at the root system prompt rather
// than at the deep node it came from, and a subagent, which brings its own
// root. A resample that was thrown away is a dead end, not a branch.
// Every number under this figure is derived from this list, so the drawing
// and the counts cannot drift apart.
const N = [
{ id:'S1', p:null, d:0, lane:0, kind:'system', t:'system' },
{ id:'u1', p:'S1', d:1, lane:0, kind:'user', t:'task' },
{ id:'a1', p:'u1', d:2, lane:0, kind:'assistant', t:'turn 1' },
{ id:'t1', p:'a1', d:3, lane:0, kind:'tool', t:'result' },
{ id:'a2', p:'t1', d:4, lane:0, kind:'assistant', t:'turn 2' },
{ id:'t2', p:'a2', d:5, lane:0, kind:'tool', t:'result' },
{ id:'a3', p:'t2', d:6, lane:0, kind:'assistant', t:'summary', leaf:true },
{ id:'xx', p:'a2', d:5, lane:1, kind:'assistant', t:'resample', dead:true },
{ id:'u2', p:'S1', d:1, lane:2, kind:'user', t:'summary in', compact:true },
{ id:'a4', p:'u2', d:2, lane:2, kind:'assistant', t:'turn 3' },
{ id:'t3', p:'a4', d:3, lane:2, kind:'tool', t:'result' },
{ id:'a5', p:'t3', d:4, lane:2, kind:'assistant', t:'submit', leaf:true },
{ id:'S2', p:null, d:0, lane:3, kind:'system', t:'subagent', root2:true },
{ id:'u3', p:'S2', d:1, lane:3, kind:'user', t:'sub-task' },
{ id:'a6', p:'u3', d:2, lane:3, kind:'assistant', t:'turn 1' },
{ id:'a7', p:'a6', d:3, lane:3, kind:'assistant', t:'return', leaf:true },
];
const byId = Object.fromEntries(N.map(n => [n.id, n]));
const pathTo = (id) => { const out = []; let c = byId[id]; while (c) { out.unshift(c.id); c = c.p ? byId[c.p] : null; } return out; };
const BRANCHES = N.filter(n => n.leaf).map((n, i) => ({ leaf: n.id, path: pathTo(n.id), i }));
const DEAD = N.filter(n => n.dead).map(n => pathTo(n.id));
// What a per-turn store would have cost: every assistant turn keeps its whole
// prompt plus its completion, so shared prefixes are copied again and again.
const copies = N.filter(n => n.kind === 'assistant')
.reduce((sum, n) => sum + pathTo(n.id).length, 0);
const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let sel = 0, timer = 0, visible = true; // sel: 0..BRANCHES.length-1, or -1 for all
const draw = () => {
const W = Math.max(320, r.clientWidth);
const bd = V('--border-color','rgba(128,128,128,.3)');
const mu = V('--muted-color','rgba(128,128,128,.8)');
const tx = V('--text-color','#16181d');
const sf = V('--surface-bg','#fff');
const mono = 'ui-monospace,SFMono-Regular,Menlo,monospace';
const KC = { system: P(2), user: P(1), assistant: P(0), tool: P(3) };
const narrow = W < 620;
const padL = 16, padR = 16, padT = 22;
const maxD = Math.max(...N.map(n => n.d));
const colW = (W - padL - padR) / (maxD + 1);
const laneH = narrow ? 56 : 62;
const laneY = [0, 0.62, 1.75, 3.0]; // lane 1 tucks under lane 0
const H = padT + laneY[3]*laneH + laneH + 26;
const X = (n) => Math.round(padL + colW*n.d + colW*0.5);
const Y = (n) => Math.round(padT + laneY[n.lane]*laneH + 16);
const active = sel >= 0 ? new Set(BRANCHES[sel].path) : null;
const onPath = (id) => !active || active.has(id);
let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="A rollout trace as a graph of ${N.length} unique messages. Three root-to-leaf branches are trainable samples; one resampled turn is a dead end. The compacted continuation reattaches at the root system prompt.">`;
// edges
N.forEach(n => {
if (!n.p) return;
const q = byId[n.p];
const x1 = X(q), y1 = Y(q), x2 = X(n), y2 = Y(n);
const lit = onPath(n.id) && onPath(n.p) && !n.dead;
const c = n.dead ? mu : (n.compact ? KC.user : KC.assistant);
const mx = (x1 + x2) / 2;
s += `<path d="M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}" fill="none"
stroke="${c}" stroke-opacity="${n.dead ? .35 : (lit ? .9 : .18)}"
stroke-width="${n.dead ? 1.2 : (lit ? 2.4 : 1.3)}"
${n.dead ? 'stroke-dasharray="4 3"' : ''}/>`;
});
// nodes
N.forEach(n => {
const x = X(n), y = Y(n), c = n.dead ? mu : KC[n.kind];
const lit = onPath(n.id) && !n.dead;
const op = n.dead ? .6 : (lit ? 1 : .22);
if (n.kind === 'system') {
s += `<rect x="${x-6}" y="${y-6}" width="12" height="12" rx="3" fill="${c}" fill-opacity="${op}" stroke="${c}" stroke-opacity="${op}"/>`;
} else if (n.kind === 'user') {
s += `<circle cx="${x}" cy="${y}" r="5.5" fill="${sf}" stroke="${c}" stroke-width="2" stroke-opacity="${op}"/>`;
} else if (n.kind === 'tool') {
s += `<path d="M ${x} ${y-6} L ${x+6} ${y} L ${x} ${y+6} L ${x-6} ${y} z" fill="${c}" fill-opacity="${op}"/>`;
} else {
s += `<circle cx="${x}" cy="${y}" r="6" fill="${c}" fill-opacity="${op}"/>`;
}
if (n.leaf && lit) s += `<circle cx="${x}" cy="${y}" r="11" fill="none" stroke="${c}" stroke-opacity=".4"/>`;
s += `<text x="${x}" y="${y+19}" text-anchor="middle" font-size="9.5"
fill="${n.dead ? mu : (lit ? tx : mu)}" fill-opacity="${n.dead ? .6 : (lit ? .85 : .3)}">${esc(n.t)}</text>`;
});
// the two structural callouts
const u2 = byId['u2'], S2 = byId['S2'];
s += `<text x="${X(u2)+14}" y="${Y(u2)-8}" font-size="10" fill="${mu}" font-family="${mono}">compaction rejoins the root</text>`;
s += `<text x="${X(S2)-4}" y="${Y(S2)-13}" font-size="10" fill="${mu}" font-family="${mono}">subagent: its own root</text>`;
const xx = byId['xx'];
s += `<text x="${X(xx)+14}" y="${Y(xx)+4}" font-size="10" fill="${mu}" fill-opacity=".75" font-family="${mono}">discarded</text>`;
s += '</svg>';
const bl = sel >= 0 ? BRANCHES[sel] : null;
const score = `<div class="rg-score">
<span class="rg-stat"><b>${N.length}</b><span>unique messages</span></span>
<span class="rg-stat"><b>${BRANCHES.length}</b><span>branches, so ${BRANCHES.length} samples</span></span>
<span class="rg-stat"><b>${copies}</b><span>copies if stored per turn</span></span>
<span class="rg-stat"><b>${bl ? bl.path.length : '—'}</b><span>${bl ? 'messages in this branch' : 'showing every branch'}</span></span>
</div>`;
const legend = `<div class="rg-lg"><span class="rg-lg-t">One node is one message</span>`
+ `<span class="rg-k"><span class="rg-sw" style="background:${KC.system};border-radius:3px"></span>system</span>`
+ `<span class="rg-k"><span class="rg-sw" style="border:2px solid ${KC.user};border-radius:50%"></span>user</span>`
+ `<span class="rg-k"><span class="rg-sw" style="background:${KC.assistant};border-radius:50%"></span>assistant, the sampled part</span>`
+ `<span class="rg-k"><span class="rg-sw" style="background:${KC.tool};transform:rotate(45deg)"></span>tool result</span>`
+ `</div>`;
const note = `<div class="rg-nt">Turns are linked by exact token prefix, which works the same way in every harness.</div>`;
r.innerHTML =
`<div class="rg-pick" role="group" aria-label="Highlight one branch">`
+ BRANCHES.map((b,i) => `<button type="button" class="rg-btn" data-b="${i}" aria-pressed="${sel===i}">Branch ${i+1}</button>`).join('')
+ `<button type="button" class="rg-btn" data-b="-1" aria-pressed="${sel===-1}">All</button>`
+ `</div>` + s + score + legend + note;
r.querySelectorAll('.rg-btn').forEach(b => b.addEventListener('click', () => {
sel = +b.dataset.b; clearInterval(timer); timer = 0; draw();
}));
};
draw();
if (!reduce) {
timer = setInterval(() => {
if (!visible) return;
sel = (sel + 1) % BRANCHES.length;
draw();
}, 2800);
}
if ('IntersectionObserver' in window && !reduce) {
new IntersectionObserver((es) => { visible = es.some(e => e.isIntersecting); },
{ rootMargin:'80px' }).observe(r);
}
// Colours are read when drawing, so redraw when the reader switches theme.
new MutationObserver(draw).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
if ('ResizeObserver' in window) {
let t; new ResizeObserver(() => { clearTimeout(t); t = setTimeout(draw, 120); }).observe(r);
} else window.addEventListener('resize', draw);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot();
})();
</script>