Place two tokens anywhere in the sequence and watch what happens to their attention score. Each token's query or key vector is rotated by an angle proportional to its position. The dot product of the rotated vectors — the raw attention score — depends only on how far apart the tokens are, not on their absolute positions.
<!-- Main app container with controls, canvas and stats -->
<div class="rope-app">
<p class="intro-text">Each token's query/key vector is rotated by an angle proportional to its position. The dot product (attention score) depends only on the gap between positions — not on where they sit in the sequence.</p>
<div class="controls-grid">
<div class="control-group">
<label for="posA">Token A position <span id="valA" class="val-badge">0</span></label>
<input type="range" id="posA" min="0" max="15" value="2" step="1" aria-label="Token A position">
</div>
<div class="control-group">
<label for="posB">Token B position <span id="valB" class="val-badge">5</span></label>
<input type="range" id="posB" min="0" max="15" value="5" step="1" aria-label="Token B position">
</div>
</div>
<!-- Canvas where rotated vectors are drawn -->
<canvas id="ropeCanvas" width="480" height="220" aria-label="Vector diagram showing rotated query and key vectors"></canvas>
<div class="stats-row">
<div class="stat-box">
<span class="stat-label">Gap |m−n|</span>
<span class="stat-value" id="gapVal">3</span>
</div>
<div class="stat-box highlight">
<span class="stat-label">Attention score</span>
<span class="stat-value" id="dotVal">0.00</span>
</div>
<div class="stat-box">
<span class="stat-label">Relative angle</span>
<span class="stat-value" id="angleVal">0.00</span>
</div>
</div>
<p class="insight" id="insightMsg"></p>
<div class="btns">
<button id="btnTogether" type="button">Move together</button>
<button id="btnApart" type="button">Move apart</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
/* Box-sizing reset for all elements */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.rope-app { max-width: 500px; margin: 0 auto; }
.intro-text { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
/* Two-column grid for the position sliders */
.controls-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .6rem; }
.control-group label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; font-weight: 600; margin-bottom: .3rem; }
.val-badge { background: #1d3557; color: #fff; border-radius: 99px; padding: .05rem .45rem; font-size: .78rem; min-width: 1.6rem; text-align: center; }
input[type=range] { width: 100%; accent-color: #1d3557; }
/* Canvas styles: full width, light border, light background */
#ropeCanvas { display: block; width: 100%; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; margin: .4rem 0; }
/* Stats row: three boxes showing gap, dot product and angle */
.stats-row { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
.stat-box { flex: 1; min-width: 90px; background: #e8eef3; border-radius: 8px; padding: .35rem .6rem; text-align: center; border: 1px solid #cdd9e3; }
.stat-box.highlight { background: #1d3557; color: #fff; border-color: #1d3557; }
.stat-box.highlight .stat-label { color: #aac0d8; }
.stat-label { display: block; font-size: .72rem; color: #6b8094; text-transform: uppercase; letter-spacing: .04em; }
.stat-value { display: block; font-size: 1.1rem; font-weight: 700; margin-top: .15rem; }
/* Insight message and button row styles */
.insight { font-size: .84rem; min-height: 1.3em; color: #1d3557; font-weight: 500; margin: .3rem 0 .6rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// RoPE: rotate q by m*theta, k by n*theta. Then q_rot · k_rot = f(n-m only).
// This is the core property: dot product depends on relative position, not absolute.
const THETA = Math.PI / 10; // base rotation frequency per position step
const canvas = document.getElementById('ropeCanvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Fixed unit query and key vectors (same for every position — only rotation changes)
const Q = [0.85, 0.53];
const K = [0.60, 0.80];
// 2D rotation by angle: standard rotation matrix
function rotate(v, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
return [c * v[0] - s * v[1], s * v[0] + c * v[1]];
}
// standard 2D dot product
function dot(a, b) { return a[0] * b[0] + a[1] * b[1]; }
function getPositions() {
return {
m: parseInt(document.getElementById('posA').value, 10),
n: parseInt(document.getElementById('posB').value, 10)
};
}
// Recompute and redraw everything when sliders change
function draw() {
const { m, n } = getPositions();
const qRot = rotate(Q, m * THETA);
const kRot = rotate(K, n * THETA);
const gap = Math.abs(n - m);
const score = dot(qRot, kRot);
const relAngle = gap * THETA;
// Update all numeric readouts
document.getElementById('valA').textContent = m;
document.getElementById('valB').textContent = n;
document.getElementById('gapVal').textContent = gap;
document.getElementById('dotVal').textContent = score.toFixed(3);
document.getElementById('angleVal').textContent = (relAngle * 180 / Math.PI).toFixed(1) + '°';
// Insight message depends on relative gap
const msg = gap === 0
? 'Same position — vectors align, score is maximum.'
: (gap <= 3
? i18nFmt('Gap of %gap% — vectors are close, score is high.', { gap })
: i18nFmt('Gap of %gap% — vectors have rotated apart, score drops.', { gap }));
document.getElementById('insightMsg').textContent = msg;
ctx.clearRect(0, 0, W, H);
// Draw from origin at canvas center
const cx = W / 2, cy = H / 2;
const scale = 90;
// Light axes for reference
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(cx - scale * 1.3, cy); ctx.lineTo(cx + scale * 1.3, cy); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx, cy - scale * 1.3); ctx.lineTo(cx, cy + scale * 1.3); ctx.stroke();
// Draw q and k rotated vectors with arrowheads and labels
function drawVec(v, color, label) {
const x = cx + v[0] * scale;
const y = cy - v[1] * scale;
ctx.strokeStyle = color;
ctx.lineWidth = 2.4;
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(x, y); ctx.stroke();
// Draw arrowhead at tip of vector
const angle = Math.atan2(-(y - cy), x - cx);
const AL = 9, AW = 0.38;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x - AL * Math.cos(angle - AW), y + AL * Math.sin(angle - AW));
ctx.lineTo(x - AL * Math.cos(angle + AW), y + AL * Math.sin(angle + AW));
ctx.closePath(); ctx.fill();
ctx.fillStyle = color;
ctx.font = '700 13px system-ui, sans-serif';
ctx.fillText(label, x + 8, y - 6);
}
drawVec(qRot, '#e63946', 'q (A)');
drawVec(kRot, '#457b9d', 'k (B)');
// Orange arc showing the angle between the two vectors
const dotClamped = Math.max(-1, Math.min(1, score));
const arcAngleQ = Math.atan2(qRot[1], qRot[0]);
const arcAngleK = Math.atan2(kRot[1], kRot[0]);
ctx.strokeStyle = '#f4a261';
ctx.lineWidth = 2;
ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.arc(cx, cy, scale * 0.38, -arcAngleQ, -arcAngleK, arcAngleQ > arcAngleK);
ctx.stroke();
ctx.setLineDash([]);
// Draw a dot at the origin
ctx.fillStyle = '#1d3557';
ctx.beginPath(); ctx.arc(cx, cy, 4, 0, Math.PI * 2); ctx.fill();
}
document.getElementById('posA').addEventListener('input', draw);
document.getElementById('posB').addEventListener('input', draw);
// Preset: same position — score should be maximum
document.getElementById('btnTogether').addEventListener('click', () => {
document.getElementById('posA').value = 4;
document.getElementById('posB').value = 4;
draw();
});
// Preset: wide gap — score drops
document.getElementById('btnApart').addEventListener('click', () => {
document.getElementById('posA').value = 1;
document.getElementById('posB').value = 10;
draw();
});
// Reset to default positions
document.getElementById('btnReset').addEventListener('click', () => {
document.getElementById('posA').value = 2;
document.getElementById('posB').value = 5;
draw();
});
draw();
<!-- Main app container with controls, canvas and stats -->
<div class="rope-app">
<p class="intro-text">Each token's query/key vector is rotated by an angle proportional to its position. The dot product (attention score) depends only on the gap between positions — not on where they sit in the sequence.</p>
<div class="controls-grid">
<div class="control-group">
<label for="posA">Token A position <span id="valA" class="val-badge">0</span></label>
<input type="range" id="posA" min="0" max="15" value="2" step="1" aria-label="Token A position">
</div>
<div class="control-group">
<label for="posB">Token B position <span id="valB" class="val-badge">5</span></label>
<input type="range" id="posB" min="0" max="15" value="5" step="1" aria-label="Token B position">
</div>
</div>
<!-- Canvas where rotated vectors are drawn -->
<canvas id="ropeCanvas" width="480" height="220" aria-label="Vector diagram showing rotated query and key vectors"></canvas>
<div class="stats-row">
<div class="stat-box">
<span class="stat-label">Gap |m−n|</span>
<span class="stat-value" id="gapVal">3</span>
</div>
<div class="stat-box highlight">
<span class="stat-label">Attention score</span>
<span class="stat-value" id="dotVal">0.00</span>
</div>
<div class="stat-box">
<span class="stat-label">Relative angle</span>
<span class="stat-value" id="angleVal">0.00</span>
</div>
</div>
<p class="insight" id="insightMsg"></p>
<div class="btns">
<button id="btnTogether" type="button">Move together</button>
<button id="btnApart" type="button">Move apart</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
/* Box-sizing reset for all elements */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.rope-app { max-width: 500px; margin: 0 auto; }
.intro-text { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
/* Two-column grid for the position sliders */
.controls-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .6rem; }
.control-group label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; font-weight: 600; margin-bottom: .3rem; }
.val-badge { background: #1d3557; color: #fff; border-radius: 99px; padding: .05rem .45rem; font-size: .78rem; min-width: 1.6rem; text-align: center; }
input[type=range] { width: 100%; accent-color: #1d3557; }
/* Canvas styles: full width, light border, light background */
#ropeCanvas { display: block; width: 100%; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; margin: .4rem 0; }
/* Stats row: three boxes showing gap, dot product and angle */
.stats-row { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
.stat-box { flex: 1; min-width: 90px; background: #e8eef3; border-radius: 8px; padding: .35rem .6rem; text-align: center; border: 1px solid #cdd9e3; }
.stat-box.highlight { background: #1d3557; color: #fff; border-color: #1d3557; }
.stat-box.highlight .stat-label { color: #aac0d8; }
.stat-label { display: block; font-size: .72rem; color: #6b8094; text-transform: uppercase; letter-spacing: .04em; }
.stat-value { display: block; font-size: 1.1rem; font-weight: 700; margin-top: .15rem; }
/* Insight message and button row styles */
.insight { font-size: .84rem; min-height: 1.3em; color: #1d3557; font-weight: 500; margin: .3rem 0 .6rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// RoPE: rotate q by m*theta, k by n*theta. Then q_rot · k_rot = f(n-m only).
// This is the core property: dot product depends on relative position, not absolute.
const THETA = Math.PI / 10; // base rotation frequency per position step
const canvas = document.getElementById('ropeCanvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Fixed unit query and key vectors (same for every position — only rotation changes)
const Q = [0.85, 0.53];
const K = [0.60, 0.80];
// 2D rotation by angle: standard rotation matrix
function rotate(v, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
return [c * v[0] - s * v[1], s * v[0] + c * v[1]];
}
// standard 2D dot product
function dot(a, b) { return a[0] * b[0] + a[1] * b[1]; }
function getPositions() {
return {
m: parseInt(document.getElementById('posA').value, 10),
n: parseInt(document.getElementById('posB').value, 10)
};
}
// Recompute and redraw everything when sliders change
function draw() {
const { m, n } = getPositions();
const qRot = rotate(Q, m * THETA);
const kRot = rotate(K, n * THETA);
const gap = Math.abs(n - m);
const score = dot(qRot, kRot);
const relAngle = gap * THETA;
// Update all numeric readouts
document.getElementById('valA').textContent = m;
document.getElementById('valB').textContent = n;
document.getElementById('gapVal').textContent = gap;
document.getElementById('dotVal').textContent = score.toFixed(3);
document.getElementById('angleVal').textContent = (relAngle * 180 / Math.PI).toFixed(1) + '°';
// Insight message depends on relative gap
const msg = gap === 0
? 'Same position — vectors align, score is maximum.'
: (gap <= 3
? i18nFmt('Gap of %gap% — vectors are close, score is high.', { gap })
: i18nFmt('Gap of %gap% — vectors have rotated apart, score drops.', { gap }));
document.getElementById('insightMsg').textContent = msg;
ctx.clearRect(0, 0, W, H);
// Draw from origin at canvas center
const cx = W / 2, cy = H / 2;
const scale = 90;
// Light axes for reference
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(cx - scale * 1.3, cy); ctx.lineTo(cx + scale * 1.3, cy); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx, cy - scale * 1.3); ctx.lineTo(cx, cy + scale * 1.3); ctx.stroke();
// Draw q and k rotated vectors with arrowheads and labels
function drawVec(v, color, label) {
const x = cx + v[0] * scale;
const y = cy - v[1] * scale;
ctx.strokeStyle = color;
ctx.lineWidth = 2.4;
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(x, y); ctx.stroke();
// Draw arrowhead at tip of vector
const angle = Math.atan2(-(y - cy), x - cx);
const AL = 9, AW = 0.38;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x - AL * Math.cos(angle - AW), y + AL * Math.sin(angle - AW));
ctx.lineTo(x - AL * Math.cos(angle + AW), y + AL * Math.sin(angle + AW));
ctx.closePath(); ctx.fill();
ctx.fillStyle = color;
ctx.font = '700 13px system-ui, sans-serif';
ctx.fillText(label, x + 8, y - 6);
}
drawVec(qRot, '#e63946', 'q (A)');
drawVec(kRot, '#457b9d', 'k (B)');
// Orange arc showing the angle between the two vectors
const dotClamped = Math.max(-1, Math.min(1, score));
const arcAngleQ = Math.atan2(qRot[1], qRot[0]);
const arcAngleK = Math.atan2(kRot[1], kRot[0]);
ctx.strokeStyle = '#f4a261';
ctx.lineWidth = 2;
ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.arc(cx, cy, scale * 0.38, -arcAngleQ, -arcAngleK, arcAngleQ > arcAngleK);
ctx.stroke();
ctx.setLineDash([]);
// Draw a dot at the origin
ctx.fillStyle = '#1d3557';
ctx.beginPath(); ctx.arc(cx, cy, 4, 0, Math.PI * 2); ctx.fill();
}
document.getElementById('posA').addEventListener('input', draw);
document.getElementById('posB').addEventListener('input', draw);
// Preset: same position — score should be maximum
document.getElementById('btnTogether').addEventListener('click', () => {
document.getElementById('posA').value = 4;
document.getElementById('posB').value = 4;
draw();
});
// Preset: wide gap — score drops
document.getElementById('btnApart').addEventListener('click', () => {
document.getElementById('posA').value = 1;
document.getElementById('posB').value = 10;
draw();
});
// Reset to default positions
document.getElementById('btnReset').addEventListener('click', () => {
document.getElementById('posA').value = 2;
document.getElementById('posB').value = 5;
draw();
});
draw();
Move both tokens together and the score stays constant. Move them apart and it drops. That is RoPE working: absolute position disappears from the score, relative distance is all that remains.
Comments
Loading comments...