| <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> |
|
|