مدل زبانی از جای توکن در جمله بی‌خبر است و تنها چیزی که به او می‌گوید «این توکن هفتم جمله است»، چرخاندن بردار پرسش و کلید است. RoPE این کار را با یک ضرب ساده انجام می‌دهد و همین ایده، شصت‌وچهار بازه‌ی فاصله را هم‌زمان می‌سازد. در این نوشته RoPE را در سی خط پایتون بدون هیچ کتابخانه‌ای می‌نویسیم، سه ویژگی‌اش را با عدد می‌سنجیم، و سپس نشان می‌دهیم پرچم‌های --rope-freq-base و --rope-scaling در llama.cpp دقیقاً همین محاسبه را دستکاری می‌کنند. همه‌ی عددهای این پست خروجی واقعی همین کد روی همین ماشین است.

چرخش، نه جمع: RoPE دقیقاً چه می‌کند

ایده‌ی رایج این است که موقعیت توکن به بردار آن اضافه می‌شود، همان کاری که کدگذاری موقعیتی سینوسی در مقاله‌ی اصلی ترنسفورمر [۱] می‌کرد. RoPE مسیر دیگری می‌رود: به‌جای جمع کردن، بردار را می‌چرخاند [۲].

بردار پرسش و بردار کلید هر کدام به جفت‌های دومانویی شکسته می‌شوند. جفت i با زاویه‌ی pos × 10000^(-2i/d) می‌چرخد، که pos موقعیت توکن و d بُعد سرِ توجه است.

نکته‌ی مهم این است که این کار هیچ بردار موقعیتی به پارامترهای مدل اضافه نمی‌کند. RoPE هیچ وزن تازه‌ای ندارد و چیزی برای آموزش‌دادن وجود ندارد. تنها چیزی که عوض می‌شود، خودِ بردارهای پرسش و کلید درست پیش از ضرب داخلی است.

همین مدل با نام RoFormer امروز در کتابخانه‌ی Transformers در دسترس است [۵].

همین چند خط، تمام پیاده‌سازی RoPE است. آن را در فایل rope_check.py ذخیره کنید؛ نیازی به نصب هیچ بسته‌ای نیست، چون تنها کتابخانه‌ی استاندارد math را صدا می‌زند.

# ذخیره‌ی این فایل به نام rope_check.py در یک پوشه‌ی خالی، و اجرای آن:
$ python3 rope_check.py

import math

BASE = 10000.0

def rope(x, pos, base=BASE):
    # هر جفت (i, i+d/2) جداگانه می‌چرخد؛ d باید زوج باشد
    d = len(x)
    out = []
    for i in range(d // 2):
        t = pos * base ** (-2.0 * i / d)   # زاویه‌ی جفت i در موقعیت pos
        c, s = math.cos(t), math.sin(t)
        a, b = x[i], x[i + d // 2]
        out += [a * c - b * s, a * s + b * c]   # چرخش در صفحه‌ی جفت
    return out

def dot(a, b):
    return sum(p * q for p, q in zip(a, b))

فقط فاصله مهم است، نه موقعیت مطلق

دلیلی که RoPE کار می‌کند یک خاصیت هندسی است. ضرب داخلی دو بردار چرخیده، فقط به اختلاف زاویه‌ی آن‌ها بستگی دارد، نه به زاویه‌ی مطلق هرکدام. اگر هر دو بردار را با یک مقدار یکسان جلو ببریم، ضرب داخلی دقیقاً ثابت می‌ماند.

این را اندازه می‌گیریم. یک بردار با بُعد ۱۲۸ می‌سازیم و ضرب داخلی‌اش را با خودش، در فاصله‌ی ثابت سه، حساب می‌کنیم. تنها چیزی که تغییر می‌کند، جابه‌جایی مشترک دو بردار است.

$ python3 rope_check.py

=== ۲. جابه‌جایی مشترک هیچ اثری ندارد ===
   جابه‌جایی      0 -> -1.3353023005
   جابه‌جایی    100 -> -1.3353023005
   جابه‌جایی   5000 -> -1.3353023005
   جابه‌جایی  40000 -> -1.3353023005

=== ۳. چرخش، طول بردار را حفظ می‌کند ===
   ||x||^2 = 1.5625000000   ||RoPE(x,7)||^2 = 1.5625000000
   اختلاف مطلق = 2.22e-16

عدد تا نه رقم اعشار یکسان است، در هر چهار جابه‌جایی. اگر RoPE موقعیت مطلق را کد می‌کرد، این چهار خط باید متفاوت می‌بودند.

همین خاصیت را مقاله‌ی RoFormer [۲] «وابستگی صریح به موقعیت نسبی» می‌نامد.

سطر آخر همان خروجی، ویژگی دوم را نشان می‌دهد: چرخش طول بردار را حفظ می‌کند. مربع طول پیش و پس از چرخش هر دو ۱٫۵۶۲۵ است و اختلاف در حد ۲٫۲۲ ضرب در ۱۰ به توان منفی ۱۶ می‌ماند، یعنی خطای ممیز شناور و نه بیشتر.

نردبان فرکانس: هر جفت یک بازه‌ی فاصله

بخش جالب ماجرا اینجاست که همه‌ی جفت‌ها با یک فرکانس نمی‌چرخند. هرچه شماره‌ی جفت بزرگ‌تر شود، فرکانس کمتر و دوره‌ی Alias بلندتر می‌شود. این نردبان همان چیزی است که به مدل اجازه می‌دهد هم‌زمان «کلمه‌ی قبلی» و «چند هزار توکن قبل» را ببیند.

با بُعد سرِ ۱۲۸، شصت‌وچهار جفت داریم و هر جفت یک بازه‌ی فاصله‌ی متفاوت می‌سازد. جدول زیر از اجرای همین کد آمده است.

شماره‌ی جفتفرکانس در هر توکنفاصله‌ی Alias شدن بر حسب توکن
۰1.00000000۶٫۳
۱0.86596432۷٫۳
۱۶0.10000000۶۲٫۸
۳۲0.01000000۶۲۸٫۳
۴۸0.00100000۶۲۸۳٫۲
۶۳0.00011548۵۴۴۱۰٫۱

سطر آخر مهم‌ترین سطر جدول است. جفت ۶۳ کندترین چرخش را دارد و تا فاصله‌ی ۵۴۴۱۰ توکن Alias نمی‌شود. یعنی اگر دو توکن دقیقاً ۵۴۴۱۰ واحد فاصله داشته باشند، این جفت دوباره به همان زاویه‌ی اول برمی‌گردد و آن دو توکن برای این جفت یکسان به نظر می‌رسند.

همین پدیده دلیل فنیِ این است که چرا مدل بیرون از طول زمینه‌ی آموزش خودش خراب می‌شود. بُعد سر این عدد را جابه‌جا می‌کند اما محدود: با بُعد ۸ کندترین جفت در ۶۲۸۳ توکن Alias می‌شود، با بُعد ۶۴ در ۴۷۱۱۷ و با بُعد ۱۲۸ در ۵۴۴۱۰. بُعد ۲۵۶ این عدد را فقط به ۵۸۴۷۰ می‌رساند. یعنی دو برابر کردن بُعد سر، حدود ۷ درصد به برد بی‌Alias اضافه می‌کند.

پرچم‌های llama.cpp دقیقاً همین محاسبه را دستکاری می‌کنند

در llama.cpp پرچم‌های RoPE دقیقاً همین فرمول را تغییر می‌دهند. این بخش به کار روزمره‌ی شما می‌آید. راهنمای خط فرمان [۴] نام و پیش‌فرض هر کدام را فهرست می‌کند.

مستندات ترنسفورمرز [۶] همین خانواده را با شش نام خودش تعریف می‌کند. حالت default همان RoPE استاندارد لاما است. چهار نوع دیگر باقی می‌ماند که هرکدام بخشی از نردبان فرکانس را دستکاری می‌کنند: linear همه‌ی جفت‌ها را یکسان می‌کشد، dynamic فقط پایه را بالا می‌برد، yarn جفت‌های پرفرکانس را کنار می‌گذارد و کم‌فرکانس‌ها را درون‌یابی می‌کند، و longrope و llama3 برای مدل‌هایی است که پیکربندی مخصوص خودشان را دارند.

بالا بردن پایه با --rope-freq-base همه‌ی فرکانس‌ها را یکجا کش می‌دهد. همان تابع period را با پایه‌ی ۵۰۰۰۰۰ دوباره اجرا می‌کنیم:

$ python3 rope_check.py

=== ۵. اثر --rope-freq-base روی همان محاسبه ===
   پایه        کندترین جفت در d=128
          10000            54410.1 توکن
         500000          2559195.5 توکن

یعنی برد بی‌Alias در بُعد ۱۲۸ از ۵۴۴۱۰ به ۲۵۵۹۱۹۵ توکن می‌رسد، یعنی ۴۷ برابر. همین روش را مستندات ترنسفورمرز [۶] با نام dynamic و مقاله‌ی YaRN [۳] به روش NTK-aware نسبت می‌دهند.

کم‌کردن موقعیت با --rope-scaling linear کار دیگری می‌کند: هر موقعیت را بر ضریب تقسیم می‌کند، پس دو توکنِ یک واحد فاصله، دو واحد فاصله‌ی دیده می‌شوند. روش درون‌یابی موقعیتی [۸] همین ایده را پیش از YaRN روی مدل‌های لاما آزمایش کرد. هزینه‌ی آن را می‌توان دقیق دید:

$ python3 rope_check.py

=== ۶. هزینه‌ی linear: دو توکنِ مجاور چقدر شبیه‌تر می‌شوند ===
   فاصله‌ی واقعی   ضریب ۱      ضریب ۲      ضریب ۴
              1        0.9471        1.3996        1.5212
              2       -0.3846        0.9471        1.3996
              4       -1.1665       -0.3846        0.9471
   با ضریب ۲، دو توکنِ یک واحد فاصله، ۰٫۹۴۷۱ می‌دهند؛ همان عددی که ضریب ۱ به فاصله‌ی ۲ می‌دهد.

ستون آخر را بخوانید: با ضریب ۲، ضرب داخلیِ دو توکنِ یک واحد فاصله برابر ۰٫۹۴۷۱ می‌شود، دقیقاً همان عددی که در حالت عادی فاصله‌ی دو توکن می‌دهد. یعنی مدل دیگر نمی‌تواند «یک کلمه فاصله» را از «دو کلمه فاصله» تشخیص دهد. YaRN [۳] دقیقاً برای همین ساخته شده: جفت‌های پرفرکانس را دست‌نخورده می‌گذارد و فقط کم‌فرکانس‌ها را درون‌یابی می‌کند، پس تفکیک محلی حفظ می‌شود. کد آن در مخزن مرجع [۷] است.

تله‌ی عملی در همین‌جاست. راهنمای llama.cpp [۴] تصریح می‌کند که --rope-scale N زمینه را ضریب N بزرگ می‌کند، ولی --rope-freq-scale N زمینه را ضریب معکوسِ N بزرگ می‌کند. این دو، یک تنظیم‌اند که در دو جهت نوشته شده‌اند. اگر --rope-freq-scale 2 بدهید در حالی که قصد دوبرابر کردن دارید، موقعیت‌ها به نیمی از بازه فشرده می‌شوند و هیچ هشداری هم نمی‌گیرید.

قاعده‌ی عملی ساده است: ضریب را یک‌بار و فقط با --rope-scale بنویسید، و هرگز آن را با --rope-freq-scale در یک خط قاطی نکنید.

این را کجا به کار بببریم

سه موقعیت هست که این محاسبه به تصمیم عملی تبدیل می‌شود.

اول: وقتی مدل در طول زمینه‌ی بزرگ خروجی تکراری یا بی‌معنا می‌دهد، اولین چیزی که باید بخوانید، مقدار rope_freq_base چاپ‌شده در خروجی بارگذاری مدل است. عدد ۱۰۰۰۰ روی مدلی که با زمینه‌ی بلند آموزش دیده، معمولاً یعنی پیکربندی در فراداده‌ی مدل درست خوانده نشده است.

دوم: اگر می‌خواهید زمینه را بیش از طول آموزش مدل ببرید، --rope-scaling yarn با --yarn-orig-ctx روی طول آموزش، نقطه‌ی شروع درست است، نه linear. دلیلش همان حفظ تفکیک محلی است که در بخش قبل اندازه گرفتیم.

سوم: اگر روی متن فارسی کار می‌کنید، بدانید این محاسبه روی توکن انجام می‌شود، نه روی کلمه. یعنی طول زمینه‌ی شما بر حسب توکن فارسی سنجیده می‌شود و فارسی در هر کلمه توکن بیشتری می‌سوزاند؛ همان نسبتی که در پست توکن و پنجره‌ی زمینه اندازه گرفته شد. پس زمینه‌ی ۳۲۷۶۸ توکنی شما روی متن فارسی با همان زمینه روی متن انگلیسی یکی نیست.

در پست چطور توجه کار می‌کند ضرب داخلی و امتیازدهی را از صفر ساختیم. RoPE همان ضرب داخلی را قبل از محاسبه، با یک چرخش تغییر می‌دهد.

منابع

  1. Attention Is All You Need — Vaswani و همکاران، ۱۲ ژوئن ۲۰۱۷، بازبینی ۲ اوت ۲۰۲۳ (arXiv:1706.03762)
  2. RoFormer: Enhanced Transformer with Rotary Position Embedding — Su و همکاران، ۲۰ آوریل ۲۰۲۱، بازبینی ۸ نوامبر ۲۰۲۳ (arXiv:2104.09864)
  3. YaRN: Efficient Context Window Extension of Large Language Models — Peng و همکاران، ۳۱ اوت ۲۰۲۳ (arXiv:2309.00071)
  4. راهنمای خط فرمان llama-cli — فهرست رسمی پرچم‌های RoPE در ggml-org/llama.cpp
  5. RoFormer — مستندات مدل در Transformers، ۲۰ مه ۲۰۲۱
  6. Rotary embeddings utilities — جدول انواع RoPE و پارامترهای آن در Transformers
  7. مخزن jquesnelle/yarn — پیاده‌سازی مرجع YaRN
  8. Extending Context Window of LLMs via Positional Interpolation — Chen و همکاران، ۲۷ ژوئن ۲۰۲۳ (arXiv:2306.15595)