Spaces:
Running
Running
File size: 15,234 Bytes
cfcfbaf | 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 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | /* ===================== configuration ===================== */
const C = {
gray:'#d9d9d9', purple:'#c5b3e0', blue:'#6c8ebf', teal:'#a9d7c9',
red:'#e53935', overflow:'#ef9a9a', overflowBg:'#fdecea',
text:'#555', purpleText:'#6a4fb8', blueText:'#3f669f', tealText:'#3d8f78', green:'#2e8b57', arrow:'#c4c4c4'
};
const P = 19, W = 14, H = 64; // column pitch, column width, column height (= layers)
const N = 10, LAT = 2; // positions per sender, latent steps per sender
const SEL = [[2, 7], [3, 6]]; // selected positions per sender
const SX = 130, SY = [118, 250]; // sender KV origin
const RX = 620, RY = 118, SEG = 10; // receiver KV row (LatentMAS/KVComm)
const STX = 400, STY = 250; // staging row (colored entries only)
const CY = 382, TY = 382; // CacheBack row y, token-ID row y
const LIMIT_SLOT = 16; // context limit falls before this receiver slot
const GROUP_GAP = 10; // gap between purple and blue groups in staging
const T = { s0:0, s1:1.6, s2:4.0, s3:7.0, s4:11.0, s5:13.4, s6:17.0, s7:20.0, end:23.5 };
const STAGES = [
{ t:T.s0, name:'Sender KVs', cap:'Each sender agent holds a <b>KV cache</b> — one column per position, spanning all layers.' },
{ t:T.s1, name:'Select', cap:'Positions relevant to the receiver\'s query <b>q</b> are <span style="color:#6a4fb8"><b>selected</b></span>.' },
{ t:T.s2, name:'Latent steps', cap:'Each sender generates <span style="color:#3f669f"><b>latent steps</b></span>, appending new KV entries.' },
{ t:T.s3, name:'Concatenate', cap:'<b>LatentMAS / KVComm</b>: transfer <i>all</i> positions and concatenate them into the receiver\'s KV, followed by the receiver prompt.' },
{ t:T.s4, name:'Overflow', cap:'The concatenated cache plus the receiver prompt <span style="color:#e53935"><b>exceeds the context limit</b></span>.' },
{ t:T.s5, name:'Pick & sort', cap:'<b>CacheBack</b>: keep only the selected positions and latent steps, and organize them.' },
{ t:T.s6, name:'CacheBack', cap:'The compact set is written into the receiver\'s cache — <span style="color:#2e8b57"><b>well within the context limit</b></span>.' },
{ t:T.s7, name:'Token IDs', cap:'Selected source positions can be sent as <span style="color:#6a4fb8"><b>token IDs</b></span>; latent steps must be sent as <span style="color:#3f669f"><b>continuous vectors</b></span>. The receiver prefills both to build its own state.' },
];
/* ===================== tiny timeline engine ===================== */
const NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('communication-svg');
function el(tag, attrs = {}, parent) {
const n = document.createElementNS(NS, tag);
for (const k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
const ease = {
lin: t => t,
io: t => t < .5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2,
out: t => 1 - Math.pow(1 - t, 3),
back:t => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3*Math.pow(t - 1, 3) + c1*Math.pow(t - 1, 2); },
};
const COLOR_PROPS = new Set(['fill', 'stroke']);
const ATTR = { w:'width', h:'height' };
const hex = h => [0, 2, 4].map(i => parseInt(h.replace('#', '').substr(i, 2), 16));
const norm = (k, v) => COLOR_PROPS.has(k) ? hex(v) : v;
const mix = (a, b, p) => Array.isArray(a) ? a.map((v, i) => v + (b[i] - v) * p) : a + (b - a) * p;
const actors = [];
class Actor {
constructor(node, init) {
this.node = node; this.init = {}; this.tracks = {}; this.last = {};
for (const k in init) this.init[k] = norm(k, init[k]);
actors.push(this);
}
to(t0, dur, props, e = 'io') {
for (const k in props) if (!(k in this.init)) this.init[k] = +this.node.getAttribute(ATTR[k] || k);
for (const k in props) (this.tracks[k] ||= []).push({ t0, t1: t0 + dur, v: norm(k, props[k]), e: ease[e] });
return this;
}
at(t, props) { return this.to(t, 0, props, 'lin'); }
eval(t) {
for (const k in this.init) {
let v = this.init[k];
for (const s of (this.tracks[k] || [])) {
if (t >= s.t1) { v = s.v; continue; }
if (t > s.t0) v = mix(v, s.v, s.e((t - s.t0) / (s.t1 - s.t0)));
break;
}
const out = COLOR_PROPS.has(k) ? `rgb(${v.map(Math.round).join(',')})` : (+v.toFixed(2));
if (this.last[k] !== out) { this.node.setAttribute(ATTR[k] || k, out); this.last[k] = out; }
}
}
finalize() { for (const k in this.tracks) this.tracks[k].sort((a, b) => a.t0 - b.t0); }
}
/* ===================== scene ===================== */
const defs = el('defs', {}, svg);
const mk = el('marker', { id:'ah', viewBox:'0 0 10 10', refX:5, refY:5, markerWidth:3.2, markerHeight:3.2, orient:'auto-start-reverse' }, defs);
el('path', { d:'M0,0 L10,5 L0,10 z', fill:C.arrow }, mk);
const L = {};
for (const name of ['bg', 'main', 'clone', 'over', 'labels']) L[name] = el('g', {}, svg);
const rect = (init, parent = L.main, rx = 2) => new Actor(el('rect', { rx }, parent), init);
function txt(str, x, y, o = {}, parent = L.labels) {
const n = el('text', { x, y, 'font-size':o.size || 14, 'font-weight':o.weight || 400,
fill:o.fill || C.text, 'text-anchor':o.anchor || 'start', ...(o.attrs || {}) }, parent);
n.innerHTML = str;
return new Actor(n, { opacity:o.opacity ?? 0, ...(o.init || {}) });
}
const group = (parent = L.labels, opacity = 0) => { const g = el('g', {}, parent); return [g, new Actor(g, { opacity })]; };
const colX = i => SX + i * P;
const slotX = g => RX + Math.floor(g / (N + LAT)) * ((N + LAT) * P + SEG) + (g % (N + LAT)) * P;
const limitX = slotX(LIMIT_SLOT) - (P - W) / 2;
const recvPromptX = RX + 2 * ((N + LAT) * P + SEG);
const PROMPT_W = 3 * P - (P - W);
/* --- axes --- */
{
const [g, a] = group();
el('line', { x1:SX, y1:76, x2:SX + 200, y2:76, stroke:C.arrow, 'stroke-width':5, 'marker-end':'url(#ah)' }, g);
const t1 = el('text', { x:SX, y:62, 'font-size':16, fill:'#666' }, g); t1.textContent = 'positions';
el('line', { x1:SX - 24, y1:SY[0], x2:SX - 24, y2:SY[0] + 110, stroke:C.arrow, 'stroke-width':5, 'marker-end':'url(#ah)' }, g);
const t2 = el('text', { x:SX - 34, y:SY[0] + 14, 'font-size':16, fill:'#666', 'text-anchor':'end' }, g); t2.textContent = 'layers';
a.to(0.1, .6, { opacity:1 });
}
const senderLabels = [0, 1].map(s => txt(`Sender ${s + 1} KV`, SX, SY[s] - 10, { size:15, weight:700 }).to(0.1 + s * .25, .5, { opacity:1 }));
/* --- stage 0/1/2: sender columns --- */
const senderCols = [[], []];
const scanStart = T.s1 + 0.3, scanDur = 1.8;
for (const s of [0, 1]) {
for (let i = 0; i < N + LAT; i++) {
const isLat = i >= N, y = SY[s];
const a = rect({ x:colX(i), y:y + H/2, w:W, h:0, fill:isLat ? C.blue : C.gray, opacity:1 });
if (!isLat) a.to(0.15 + s * .25 + i * .05, .45, { y, h:H }, 'out');
if (SEL[s].includes(i)) {
const ts = scanStart + scanDur * (i + 1) / (N + 1);
a.to(ts, .25, { fill:C.purple }, 'lin')
.to(ts, .15, { y:y - 4, h:H + 8 }, 'out').to(ts + .15, .25, { y, h:H });
}
senderCols[s].push(a);
}
// scan bar with query q
const scan = rect({ x:colX(-1) - 3, y:SY[s] - 6, w:W + 6, h:H + 12, fill:'#7cc4b0', opacity:0 }, L.over, 4);
scan.to(scanStart - .15, .15, { opacity:.5 }, 'lin')
.to(scanStart, scanDur, { x:colX(N) - 3 }, 'lin')
.to(scanStart + scanDur, .2, { opacity:0 }, 'lin');
const q = txt('q', colX(-1) + W/2, SY[s] + H + 18, { size:15, weight:700, fill:C.tealText, anchor:'middle', attrs:{ 'font-style':'italic' } });
q.to(scanStart - .15, .15, { opacity:1 }, 'lin')
.to(scanStart, scanDur, { x:colX(N) + W/2 }, 'lin')
.to(scanStart + scanDur, .2, { opacity:0 }, 'lin');
// latent step generation: attend over existing cache, then append a blue column
for (let k = 0; k < LAT; k++) {
const tk = T.s2 + 0.3 + k * 1.3;
const flash = rect({ x:SX - 4, y:SY[s] - 4, w:(N + k) * P - (P - W) + 8, h:H + 8, fill:C.blue, opacity:0 }, L.over, 5);
flash.to(tk, .3, { opacity:.2 }, 'out').to(tk + .35, .35, { opacity:0 });
senderCols[s][N + k].to(tk + .45, .5, { y:SY[s], h:H }, 'back');
}
}
const typeOf = (s, i) => i >= N ? 'b' : SEL[s].includes(i) ? 'p' : 'g';
const fillOf = { p:C.purple, b:C.blue, g:C.gray };
/* --- stage 3: LatentMAS / KVComm concatenation --- */
const recvTitle = txt(`LatentMAS, KVComm <tspan font-weight="400" fill="#999">· all positions</tspan>`, RX, RY - 10, { size:15, weight:700 });
recvTitle.to(T.s3, .5, { opacity:1 });
const recv = [];
for (const s of [0, 1]) for (let i = 0; i < N + LAT; i++) {
const g = s * (N + LAT) + i;
const c = rect({ x:colX(i), y:SY[s], w:W, h:H, fill:fillOf[typeOf(s, i)], opacity:0 }, L.clone);
const t0 = T.s3 + 0.4 + s * 1.1 + i * .06;
c.at(t0, { opacity:1 }).to(t0, .9, { x:slotX(g), y:RY });
recv.push(c);
}
const recvPrompt = rect({ x:recvPromptX + 60, y:RY, w:PROMPT_W, h:H, fill:C.teal, opacity:0 }, L.clone);
recvPrompt.to(T.s3 + 3.0, .7, { x:recvPromptX, opacity:1 }, 'out');
/* --- stage 4: context limit & overflow --- */
const overBg = rect({ x:limitX, y:RY - 6, w:recvPromptX + PROMPT_W + 8 - limitX, h:H + 12, fill:C.overflowBg, opacity:0 }, L.bg, 4);
overBg.to(T.s4 + .8, .4, { opacity:1 });
const limitLine = new Actor(el('line', { x1:limitX, x2:limitX, y1:92, stroke:C.red, 'stroke-width':2.5, 'stroke-dasharray':'8 6' }, L.labels), { y2:92, opacity:0 });
limitLine.at(T.s4 + .1, { opacity:1 }).to(T.s4 + .1, .7, { y2:478 }, 'io');
const limitText = txt('Context Limit', 0, 0, { size:17, fill:C.red, anchor:'middle', attrs:{ transform:`translate(${limitX + 16},${(RY + H + CY) / 2 + 6}) rotate(90)` } });
limitText.to(T.s4 + .6, .4, { opacity:1 });
recv.forEach((c, g) => { if (g >= LIMIT_SLOT) c.to(T.s4 + .9 + (g - LIMIT_SLOT) * .05, .3, { fill:C.overflow }, 'lin'); });
recvPrompt.to(T.s4 + .9 + (recv.length - LIMIT_SLOT) * .05, .3, { fill:C.overflow }, 'lin');
const overLabel = txt('✕ exceeds context', limitX + 10, RY - 10, { size:14, weight:700, fill:C.red });
overLabel.to(T.s4 + 1.4, .4, { opacity:1 });
/* --- stage 5: CacheBack — pick colored entries, then sort by type --- */
[...recv, recvPrompt, recvTitle, overBg, overLabel].forEach(a => a.to(T.s5, .5, { opacity:.3 }));
for (const s of [0, 1]) senderCols[s].forEach((a, i) => { if (typeOf(s, i) === 'g') a.to(T.s5 + .1, .5, { opacity:.3 }); });
const picks = [];
for (const s of [0, 1]) {
SEL[s].forEach(i => picks.push({ s, i, type:'p' }));
for (let k = 0; k < LAT; k++) picks.push({ s, i:N + k, type:'b' });
}
const purples = picks.filter(p => p.type === 'p'), blues = picks.filter(p => p.type === 'b');
const sortedX = p => p.type === 'p' ? STX + purples.indexOf(p) * P
: STX + (purples.length + blues.indexOf(p)) * P + GROUP_GAP;
const sortT = T.s5 + 2.2;
picks.forEach((p, j) => {
const c = rect({ x:colX(p.i), y:SY[p.s], w:W, h:H, fill:fillOf[p.type], opacity:0 }, L.clone);
c.at(T.s5 + .3, { opacity:1 }).to(T.s5 + .3, .3, { y:SY[p.s] - 8 }, 'out'); // lift out
c.to(T.s5 + .8 + j * .08, .8, { x:STX + j * P, y:STY }); // gather
if (p.type === 'b') { // sort: blues dodge down, slide, come back
c.to(sortT, .3, { y:STY + 40 }).to(sortT + .85, .3, { y:STY });
}
c.to(sortT + .35, .45, { x:sortedX(p) });
p.actor = c;
});
txt('selected + latent only', STX, STY - 10, { size:13, fill:'#888' }).to(T.s5 + .8, .5, { opacity:1 });
/* --- stage 6: write into CacheBack receiver cache --- */
txt('CacheBack', RX, CY - 10, { size:15, weight:700 }).to(T.s6, .5, { opacity:1 });
const perSender = SEL[0].length + LAT;
const cbX = (p) => {
const m = p.type === 'p' ? SEL[p.s].indexOf(p.i) : SEL[p.s].length + (p.i - N);
return RX + p.s * (perSender * P + SEG) + m * P;
};
const cbOrder = [...picks]; // per-sender: selected positions, then latent steps
cbOrder.forEach((p, j) => {
const c = rect({ x:sortedX(p), y:STY, w:W, h:H, fill:fillOf[p.type], opacity:0 }, L.clone);
const t0 = T.s6 + .3 + j * .07;
c.at(t0, { opacity:1 }).to(t0, .9, { x:cbX(p), y:CY });
});
const cbPromptX = RX + 2 * (perSender * P + SEG);
rect({ x:cbPromptX + 40, y:CY, w:PROMPT_W, h:H, fill:C.teal, opacity:0 }, L.clone).to(T.s6 + 1.5, .7, { x:cbPromptX, opacity:1 }, 'out');
txt('✓ fits', cbPromptX + PROMPT_W + 14, CY + H / 2 + 5, { size:15, weight:700, fill:C.green }).to(T.s6 + 2.3, .4, { opacity:1 });
/* --- stage 7: purple entries compress to token IDs --- */
picks.forEach((p, j) => {
const c = rect({ x:sortedX(p), y:STY, w:W, h:H, fill:fillOf[p.type], opacity:0 }, L.clone);
const t0 = T.s7 + .3 + j * .08;
c.at(t0, { opacity:1 });
if (p.type === 'p') c.to(t0, .9, { y:TY + H / 2 - 4, h:8 });
else c.to(t0, .9, { y:TY });
});
txt('Token IDs', STX + (purples.length * P - (P - W)) / 2, TY + H / 2 - 12, { size:14, weight:600, fill:C.purpleText, anchor:'middle' })
.to(T.s7 + 1.6, .4, { opacity:1 });
txt('+prefill', STX + purples.length * P + GROUP_GAP + (blues.length * P - (P - W)) / 2, TY + H + 20, { size:14, weight:600, fill:C.purpleText, anchor:'middle' })
.to(T.s7 + 2.0, .4, { opacity:1 });
/* --- legend (always visible) --- */
{
const ly = 524, items = [
{ x:130, fill:C.purple, label:'Selected for receiver query q', color:C.purpleText },
{ x:385, fill:C.gray, label:'Unselected positions', color:'#888' },
{ x:580, fill:C.blue, label:'Latent steps', color:C.blueText },
{ x:730, fill:C.teal, label:'Receiver prompt', color:C.tealText },
];
for (const it of items) {
el('rect', { x:it.x, y:ly - 13, width:16, height:16, rx:2, fill:it.fill }, svg);
const t = el('text', { x:it.x + 24, y:ly, 'font-size':14, fill:it.color }, svg); t.textContent = it.label;
}
el('line', { x1:905, y1:ly - 5, x2:935, y2:ly - 5, stroke:C.red, 'stroke-width':2.5, 'stroke-dasharray':'6 4' }, svg);
const t = el('text', { x:943, y:ly, 'font-size':14, fill:C.red }, svg); t.textContent = 'Context limit';
}
actors.forEach(a => a.finalize());
/* ===================== playback ===================== */
const button = document.getElementById('animation-play');
let t = T.end, playing = false, lastTs = null, stageIdx = -1;
function render(time) {
for (const a of actors) a.eval(time);
let idx = 0;
for (let i = 0; i < STAGES.length; i++) if (time >= STAGES[i].t) idx = i;
if (idx !== stageIdx) {
stageIdx = idx;
document.getElementById('animation-caption').innerHTML = STAGES[idx].cap;
}
}
function pause() {
playing = false;
button.textContent = '▶';
button.title = t >= T.end ? 'Replay animation' : 'Play animation';
button.setAttribute('aria-label', button.title);
}
function frame(ts) {
if (!playing) return;
if (lastTs !== null) t = Math.min(T.end, t + (ts - lastTs) / 1000);
lastTs = ts;
render(t);
if (t >= T.end) pause();
else requestAnimationFrame(frame);
}
button.onclick = () => {
if (playing) { pause(); return; }
if (t >= T.end) t = 0;
playing = true;
lastTs = null;
button.textContent = 'Ⅱ';
button.title = 'Pause animation';
button.setAttribute('aria-label', button.title);
requestAnimationFrame(frame);
};
render(t);
|