|
|
<div class="d3-rope-demo"></div> |
|
|
|
|
|
<style> |
|
|
.d3-rope-demo { |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
line-height: 1.5; |
|
|
color: var(--text-color); |
|
|
padding: 20px 0; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .subtitle { |
|
|
color: var(--text-color); |
|
|
font-size: 18px; |
|
|
font-weight: 600; |
|
|
margin-bottom: 20px; |
|
|
text-align: center; |
|
|
max-width: 600px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .sentence { |
|
|
display: flex; |
|
|
gap: 0; |
|
|
margin: 25px 0; |
|
|
flex-wrap: wrap; |
|
|
justify-content: center; |
|
|
font-size: 18px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .slider-container { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
margin: 15px 0; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .slider-label { |
|
|
font-size: 14px; |
|
|
color: var(--muted-color); |
|
|
font-weight: 500; |
|
|
min-width: 80px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .slider { |
|
|
width: 200px; |
|
|
height: 6px; |
|
|
border-radius: 3px; |
|
|
background: var(--border-color); |
|
|
outline: none; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .slider::-webkit-slider-thumb { |
|
|
appearance: none; |
|
|
width: 18px; |
|
|
height: 18px; |
|
|
border-radius: 50%; |
|
|
background: var(--primary-color); |
|
|
cursor: pointer; |
|
|
border: 2px solid var(--page-bg); |
|
|
box-shadow: 0 2px 4px var(--border-color); |
|
|
} |
|
|
|
|
|
.d3-rope-demo .slider::-moz-range-thumb { |
|
|
width: 18px; |
|
|
height: 18px; |
|
|
border-radius: 50%; |
|
|
background: var(--primary-color); |
|
|
cursor: pointer; |
|
|
border: 2px solid var(--page-bg); |
|
|
box-shadow: 0 2px 4px var(--border-color); |
|
|
} |
|
|
|
|
|
.d3-rope-demo .slider-value { |
|
|
font-size: 14px; |
|
|
color: var(--text-color); |
|
|
font-weight: 600; |
|
|
min-width: 40px; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .rotation-info { |
|
|
text-align: center; |
|
|
margin: 20px auto; |
|
|
font-size: 16px; |
|
|
font-weight: 500; |
|
|
color: var(--text-color); |
|
|
padding: 20px; |
|
|
background: var(--page-bg); |
|
|
border-radius: 8px; |
|
|
border: 1px solid var(--border-color) !important; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05); |
|
|
max-width: 500px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .equation-gap { |
|
|
height: 15px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .word-highlight { |
|
|
color: var(--primary-color); |
|
|
font-weight: 700; |
|
|
background: var(--page-bg); |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
border: 1px solid var(--border-color); |
|
|
display: inline-block; |
|
|
min-width: 60px; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .position-highlight { |
|
|
color: var(--primary-color); |
|
|
font-weight: 700; |
|
|
background: var(--page-bg); |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
border: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.d3-rope-demo .angle-highlight { |
|
|
color: var(--primary-color); |
|
|
font-weight: 600; |
|
|
font-family: 'Courier New', monospace; |
|
|
font-size: 20px; |
|
|
padding: 12px 16px; |
|
|
border-radius: 6px; |
|
|
background: var(--page-bg); |
|
|
border: 1px solid var(--border-color); |
|
|
display: inline-block; |
|
|
width: 100%; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .word { |
|
|
cursor: pointer; |
|
|
font-weight: 700; |
|
|
font-size: 18px; |
|
|
user-select: none; |
|
|
padding: 8px 12px; |
|
|
border-radius: 0; |
|
|
transition: all 0.2s ease; |
|
|
border: 1px solid var(--border-color); |
|
|
border-right: none; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .word:first-child { |
|
|
border-radius: 6px 0 0 6px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .word:last-child { |
|
|
border-radius: 0 6px 6px 0; |
|
|
border-right: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.d3-rope-demo .word:only-child { |
|
|
border-radius: 6px; |
|
|
border-right: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
|
|
|
.button { |
|
|
background: var(--primary-color)!important; |
|
|
color: var(--page-bg)!important; |
|
|
border: 1px solid var(--primary-color)!important; |
|
|
} |
|
|
|
|
|
.button--ghost { |
|
|
background: var(--page-bg)!important; |
|
|
color: var(--primary-color)!important; |
|
|
border: 1px solid var(--primary-color)!important; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .svg-container { |
|
|
margin: 0; |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.d3-rope-demo svg { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .explanation { |
|
|
max-width: 700px; |
|
|
text-align: center; |
|
|
margin-top: 20px; |
|
|
color: var(--text-color); |
|
|
font-size: 15px; |
|
|
line-height: 1.6; |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.d3-rope-demo { |
|
|
padding: 16px 0; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .sentence { |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .word { |
|
|
font-size: 16px; |
|
|
padding: 6px 10px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .svg-container { |
|
|
width: 100%; |
|
|
max-width: 400px; |
|
|
} |
|
|
|
|
|
.d3-rope-demo svg { |
|
|
width: 100%; |
|
|
height: auto; |
|
|
} |
|
|
|
|
|
.d3-rope-demo .explanation { |
|
|
font-size: 14px; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
|
|
|
<script> |
|
|
(() => { |
|
|
const bootstrap = () => { |
|
|
const scriptEl = document.currentScript; |
|
|
let container = scriptEl ? scriptEl.previousElementSibling : null; |
|
|
if (!(container && container.classList && container.classList.contains('d3-rope-demo'))) { |
|
|
const candidates = Array.from(document.querySelectorAll('.d3-rope-demo')) |
|
|
.filter((el) => !(el.dataset && el.dataset.mounted === 'true')); |
|
|
container = candidates[candidates.length - 1] || null; |
|
|
} |
|
|
if (!container) return; |
|
|
if (container.dataset) { |
|
|
if (container.dataset.mounted === 'true') return; |
|
|
container.dataset.mounted = 'true'; |
|
|
} |
|
|
|
|
|
const sentence = ["The", "quick", "brown", "fox", "jumps", "..."]; |
|
|
|
|
|
|
|
|
container.innerHTML = ` |
|
|
<div class="subtitle">RoPE rotation of the first (x₁, x₂) pair in Q/K vectors<br/> based on token position</div> |
|
|
<div class="sentence" id="sentence"></div> |
|
|
<div class="slider-container"> |
|
|
<input type="range" class="slider" id="positionSlider" min="0" max="5" step="1" value="0"> |
|
|
</div> |
|
|
<div class="svg-container"> |
|
|
<svg id="ropeSvg" width="500" height="400" viewBox="0 0 500 400"></svg> |
|
|
</div> |
|
|
|
|
|
<div class="rotation-info" id="rotationInfo"> |
|
|
<span class="word-highlight">The</span> at position <span class="position-highlight">0</span> gets rotated by |
|
|
<div class="equation-gap"></div> |
|
|
<span class="angle-highlight">θ = 0 rad (0°)</span> |
|
|
</div> |
|
|
<div class="explanation"> |
|
|
<strong>RoPE Formula:</strong> θ (theta) = position × 1 / base<sup>2 × pair_index/h_dim</sup> (pair_index=0 here) |
|
|
<br><br> |
|
|
<strong>Key insight:</strong> The first dimension pair gets the largest rotations, and the relative angle between words depends only on their distance apart. |
|
|
</div> |
|
|
`; |
|
|
|
|
|
const svg = container.querySelector('#ropeSvg'); |
|
|
const sentenceEl = container.querySelector('#sentence'); |
|
|
const slider = container.querySelector('#positionSlider'); |
|
|
const rotationInfo = container.querySelector('#rotationInfo'); |
|
|
|
|
|
const R = 140; |
|
|
const R_LABELS = 180; |
|
|
const cx = 250; |
|
|
const cy = 200; |
|
|
const ANGLE_OFFSET = 5; |
|
|
|
|
|
|
|
|
const base = 10000; |
|
|
const d = 2048; |
|
|
const m = 0; |
|
|
|
|
|
function getRopeAngle(pos) { |
|
|
return pos * (1 / Math.pow(base, (2 * m) / d)); |
|
|
} |
|
|
|
|
|
let activeIndex = 0; |
|
|
let animating = true; |
|
|
let animationTimeout = null; |
|
|
|
|
|
function renderSentence() { |
|
|
sentenceEl.innerHTML = ""; |
|
|
sentence.forEach((word, i) => { |
|
|
const span = document.createElement("span"); |
|
|
span.textContent = word; |
|
|
span.className = "word button" + (i === activeIndex ? "" : " button--ghost"); |
|
|
span.addEventListener("click", () => { |
|
|
stopAnimation(); |
|
|
activeIndex = i; |
|
|
slider.value = i; |
|
|
updateRotationInfo(); |
|
|
draw(); |
|
|
renderSentence(); |
|
|
}); |
|
|
sentenceEl.appendChild(span); |
|
|
}); |
|
|
} |
|
|
|
|
|
function draw() { |
|
|
|
|
|
svg.innerHTML = ''; |
|
|
|
|
|
|
|
|
const backgroundElements = []; |
|
|
const foregroundElements = []; |
|
|
const textElements = []; |
|
|
|
|
|
|
|
|
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); |
|
|
circle.setAttribute('cx', cx); |
|
|
circle.setAttribute('cy', cy); |
|
|
circle.setAttribute('r', R); |
|
|
circle.setAttribute('fill', 'none'); |
|
|
circle.setAttribute('stroke', 'var(--border-color)'); |
|
|
circle.setAttribute('stroke-width', '1.5'); |
|
|
circle.setAttribute('opacity', '0.6'); |
|
|
backgroundElements.push(circle); |
|
|
|
|
|
|
|
|
sentence.forEach((word, i) => { |
|
|
const theta = getRopeAngle(i) + (ANGLE_OFFSET * Math.PI / 180); |
|
|
const x = cx + R * Math.cos(theta); |
|
|
const y = cy + R * Math.sin(theta); |
|
|
|
|
|
const isActive = (i === activeIndex); |
|
|
const isGhost = i > activeIndex; |
|
|
|
|
|
|
|
|
const point = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); |
|
|
point.setAttribute('cx', x); |
|
|
point.setAttribute('cy', y); |
|
|
point.setAttribute('r', isActive ? 10 : 5); |
|
|
point.setAttribute('fill', isActive ? 'var(--primary-color)' : (isGhost ? 'var(--muted-color)' : 'var(--primary-color)')); |
|
|
point.setAttribute('stroke', isActive ? 'var(--page-bg)' : (isGhost ? 'var(--surface-bg)' : 'var(--page-bg)')); |
|
|
point.setAttribute('stroke-width', isActive ? '3' : '2'); |
|
|
point.setAttribute('opacity', isActive ? '1' : (isGhost ? '0.3' : '0.7')); |
|
|
backgroundElements.push(point); |
|
|
|
|
|
|
|
|
if (isActive) { |
|
|
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line'); |
|
|
arrow.setAttribute('x1', cx); |
|
|
arrow.setAttribute('y1', cy); |
|
|
arrow.setAttribute('x2', x); |
|
|
arrow.setAttribute('y2', y); |
|
|
arrow.setAttribute('stroke', 'var(--primary-color)'); |
|
|
arrow.setAttribute('stroke-width', '3'); |
|
|
arrow.setAttribute('stroke-linecap', 'round'); |
|
|
arrow.setAttribute('opacity', '0.8'); |
|
|
foregroundElements.push(arrow); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const centerPoint = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); |
|
|
centerPoint.setAttribute('cx', cx); |
|
|
centerPoint.setAttribute('cy', cy); |
|
|
centerPoint.setAttribute('r', 5); |
|
|
centerPoint.setAttribute('fill', 'var(--text-color)'); |
|
|
centerPoint.setAttribute('stroke', 'var(--page-bg)'); |
|
|
centerPoint.setAttribute('stroke-width', '2'); |
|
|
centerPoint.setAttribute('opacity', '0.8'); |
|
|
foregroundElements.push(centerPoint); |
|
|
|
|
|
|
|
|
if (activeIndex !== null && activeIndex > 0) { |
|
|
const theta = getRopeAngle(activeIndex) + (ANGLE_OFFSET * Math.PI / 180); |
|
|
const startAngle = ANGLE_OFFSET * Math.PI / 180; |
|
|
const endAngle = theta; |
|
|
|
|
|
|
|
|
const radius = R * 0.7; |
|
|
const startX = cx + radius * Math.cos(startAngle); |
|
|
const startY = cy + radius * Math.sin(startAngle); |
|
|
const endX = cx + radius * Math.cos(endAngle); |
|
|
const endY = cy + radius * Math.sin(endAngle); |
|
|
|
|
|
const largeArcFlag = theta > Math.PI ? 1 : 0; |
|
|
const pathData = `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`; |
|
|
|
|
|
const arc = document.createElementNS('http://www.w3.org/2000/svg', 'path'); |
|
|
arc.setAttribute('d', pathData); |
|
|
arc.setAttribute('fill', 'none'); |
|
|
arc.setAttribute('stroke', 'var(--primary-color)'); |
|
|
arc.setAttribute('stroke-width', '2.5'); |
|
|
arc.setAttribute('stroke-dasharray', '6,4'); |
|
|
arc.setAttribute('opacity', '0.8'); |
|
|
foregroundElements.push(arc); |
|
|
} |
|
|
|
|
|
|
|
|
sentence.forEach((word, i) => { |
|
|
const theta = getRopeAngle(i); |
|
|
const x = cx + R * Math.cos(theta); |
|
|
const y = cy + R * Math.sin(theta); |
|
|
|
|
|
const isActive = (i === activeIndex); |
|
|
const isGhost = i > activeIndex; |
|
|
|
|
|
|
|
|
const labelX = cx + R_LABELS * Math.cos(theta); |
|
|
const labelY = cy + R_LABELS * Math.sin(theta); |
|
|
const wordLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); |
|
|
wordLabel.setAttribute('x', labelX); |
|
|
wordLabel.setAttribute('y', labelY); |
|
|
wordLabel.setAttribute('text-anchor', 'middle'); |
|
|
wordLabel.setAttribute('dominant-baseline', 'middle'); |
|
|
wordLabel.setAttribute('fill', isActive ? 'var(--text-color)' : (isGhost ? 'var(--muted-color)' : 'var(--text-color)')); |
|
|
wordLabel.setAttribute('font-family', '-apple-system, BlinkMacSystemFont, sans-serif'); |
|
|
wordLabel.setAttribute('font-size', isActive ? '18' : '15'); |
|
|
wordLabel.setAttribute('font-weight', isActive ? '700' : '500'); |
|
|
wordLabel.setAttribute('opacity', isActive ? '1' : (isGhost ? '0.3' : '0.8')); |
|
|
wordLabel.textContent = word; |
|
|
textElements.push(wordLabel); |
|
|
}); |
|
|
|
|
|
|
|
|
if (activeIndex !== null && activeIndex > 0) { |
|
|
const theta = getRopeAngle(activeIndex) + (ANGLE_OFFSET * Math.PI / 180); |
|
|
const radius = R * 0.7; |
|
|
const angleLabelX = cx + radius * 0.5 * Math.cos(theta / 2); |
|
|
const angleLabelY = cy + radius * 0.5 * Math.sin(theta / 2); |
|
|
|
|
|
|
|
|
const angleLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); |
|
|
angleLabel.setAttribute('x', angleLabelX); |
|
|
angleLabel.setAttribute('y', angleLabelY); |
|
|
angleLabel.setAttribute('text-anchor', 'middle'); |
|
|
angleLabel.setAttribute('font-family', '-apple-system, BlinkMacSystemFont, sans-serif'); |
|
|
angleLabel.setAttribute('font-size', '13'); |
|
|
angleLabel.setAttribute('font-weight', '600'); |
|
|
|
|
|
|
|
|
const thetaSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); |
|
|
thetaSpan.setAttribute('fill', 'var(--primary-color)'); |
|
|
thetaSpan.textContent = 'θ'; |
|
|
angleLabel.appendChild(thetaSpan); |
|
|
|
|
|
|
|
|
const equalsSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); |
|
|
equalsSpan.setAttribute('fill', 'var(--primary-color)'); |
|
|
equalsSpan.setAttribute('opacity', '0.5'); |
|
|
equalsSpan.textContent = ' = '; |
|
|
angleLabel.appendChild(equalsSpan); |
|
|
|
|
|
|
|
|
const numberSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); |
|
|
numberSpan.setAttribute('fill', 'var(--primary-color)'); |
|
|
numberSpan.textContent = activeIndex.toString(); |
|
|
angleLabel.appendChild(numberSpan); |
|
|
|
|
|
textElements.push(angleLabel); |
|
|
} |
|
|
|
|
|
|
|
|
backgroundElements.forEach(el => svg.appendChild(el)); |
|
|
foregroundElements.forEach(el => svg.appendChild(el)); |
|
|
textElements.forEach(el => svg.appendChild(el)); |
|
|
} |
|
|
|
|
|
function updateRotationInfo() { |
|
|
const theta = getRopeAngle(activeIndex); |
|
|
const degrees = Math.round(theta * 180 / Math.PI); |
|
|
rotationInfo.innerHTML = ` |
|
|
<span class="word-highlight">${sentence[activeIndex]}</span> at position <span class="position-highlight">${activeIndex}</span> gets rotated by |
|
|
<div class="equation-gap"></div> |
|
|
<div class="angle-highlight"> |
|
|
<span style="color: var(--muted-color); opacity: 0.6;">θ</span> |
|
|
<span style="color: var(--muted-color); opacity: 0.4; margin: 0 8px;">=</span> |
|
|
<span style="opacity: 1;">${activeIndex}</span> |
|
|
<span style="color: var(--muted-color); opacity: 0.6;">rad</span> |
|
|
<span style="color: var(--muted-color); opacity: 0.4;">(</span> |
|
|
<span style="opacity: 1;">${degrees}°</span> |
|
|
<span style="color: var(--muted-color); opacity: 0.4;">)</span> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function stopAnimation() { |
|
|
animating = false; |
|
|
if (animationTimeout) { |
|
|
clearTimeout(animationTimeout); |
|
|
animationTimeout = null; |
|
|
} |
|
|
} |
|
|
|
|
|
function animate() { |
|
|
if (!animating) return; |
|
|
|
|
|
animationTimeout = setTimeout(() => { |
|
|
activeIndex = (activeIndex + 1) % sentence.length; |
|
|
slider.value = activeIndex; |
|
|
updateRotationInfo(); |
|
|
renderSentence(); |
|
|
draw(); |
|
|
animate(); |
|
|
}, 1500); |
|
|
} |
|
|
|
|
|
|
|
|
slider.addEventListener('input', (e) => { |
|
|
stopAnimation(); |
|
|
activeIndex = parseInt(e.target.value); |
|
|
updateRotationInfo(); |
|
|
renderSentence(); |
|
|
draw(); |
|
|
}); |
|
|
|
|
|
|
|
|
renderSentence(); |
|
|
updateRotationInfo(); |
|
|
draw(); |
|
|
animate(); |
|
|
}; |
|
|
|
|
|
if (document.readyState === 'loading') { |
|
|
document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); |
|
|
} else { |
|
|
bootstrap(); |
|
|
} |
|
|
})(); |
|
|
</script> |
|
|
|