Spaces:
Running
Running
| <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) ; | |
| 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); | |
| color: var(--page-bg); | |
| border: 1px solid var(--primary-color); | |
| } | |
| .button--ghost { | |
| background: var(--page-bg); | |
| color: var(--primary-color); | |
| border: 1px solid var(--primary-color); | |
| } | |
| .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; | |
| } | |
| /* Responsive design */ | |
| @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", "..."]; | |
| // Create the HTML structure | |
| 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; // Cercle plus grand pour les labels | |
| const cx = 250; | |
| const cy = 200; | |
| const ANGLE_OFFSET = 5; // Offset en degrés pour mieux aligner les 6 mots | |
| // RoPE parameters | |
| 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() { | |
| // Clear SVG | |
| svg.innerHTML = ''; | |
| // Create arrays to store elements for proper layering | |
| const backgroundElements = []; | |
| const foregroundElements = []; | |
| const textElements = []; | |
| // Draw circle (background) | |
| 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); | |
| // Draw all word positions | |
| sentence.forEach((word, i) => { | |
| const theta = getRopeAngle(i) + (ANGLE_OFFSET * Math.PI / 180); // Ajouter l'offset en radians | |
| const x = cx + R * Math.cos(theta); | |
| const y = cy + R * Math.sin(theta); | |
| const isActive = (i === activeIndex); | |
| const isGhost = i > activeIndex; // Éléments après la position active sont en ghost | |
| // Draw point (background) | |
| 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); | |
| // Draw arrow for active word (foreground) | |
| 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); | |
| } | |
| }); | |
| // Draw center point (foreground) | |
| 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); | |
| // Draw angle arc for active word (foreground) | |
| if (activeIndex !== null && activeIndex > 0) { | |
| const theta = getRopeAngle(activeIndex) + (ANGLE_OFFSET * Math.PI / 180); | |
| const startAngle = ANGLE_OFFSET * Math.PI / 180; | |
| const endAngle = theta; | |
| // Create arc path | |
| 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); | |
| } | |
| // Draw all text elements (top layer) | |
| 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; // Éléments après la position active sont en ghost | |
| // Draw word label on larger circle | |
| 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); | |
| }); | |
| // Add angle label (top layer) | |
| 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); | |
| // Create tspan elements for different styling | |
| 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'); | |
| // θ in primary color | |
| const thetaSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); | |
| thetaSpan.setAttribute('fill', 'var(--primary-color)'); | |
| thetaSpan.textContent = 'θ'; | |
| angleLabel.appendChild(thetaSpan); | |
| // = with reduced opacity | |
| 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); | |
| // Number in primary color | |
| 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); | |
| } | |
| // Append elements in correct order: background -> foreground -> text | |
| 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 event listener | |
| slider.addEventListener('input', (e) => { | |
| stopAnimation(); | |
| activeIndex = parseInt(e.target.value); | |
| updateRotationInfo(); | |
| renderSentence(); | |
| draw(); | |
| }); | |
| // Initialize and start | |
| renderSentence(); | |
| updateRotationInfo(); | |
| draw(); | |
| animate(); | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); | |
| } else { | |
| bootstrap(); | |
| } | |
| })(); | |
| </script> | |