Модуль 4. Позиционные представления: от синусоид к нескольким осям
Здесь можно прочитать пояснения и код имеющихся ноутбуков или скачать файлы. Код на сайте не запускается.
Скачать файлы курса (.zip) 2588,1 КБАрхив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.
Практика М4. Позиции, RoPE и расширение контекста
Основной ноутбук модуля
Скачать .ipynb module_04_notebook_rope.ipynb · 72,3 КБ
Показать текст ноутбука
Практика М4. Позиции, RoPE и расширение контекста
Здесь мы поворачиваем векторы, сравниваем способы изменения частот и проверяем позиции при генерации с кэшем. Все примеры малы и рассчитаны на CPU.
Маршруты. При первом запуске выполните все ячейки и прочитайте пояснения к результатам. При повторном — меняйте входы и проверяйте, какие тождества сохраняются. Справочные реализации собраны по темам: позиции, вращение, масштабирование, несколько осей и кэш.
Формы. Основной вход [..., T, D] хранит T векторов чётной ширины D; оси слева могут быть батчем и головами. Позиции — целочисленный вектор [T], частоты — положительный вектор [D/2]. Численные эталоны используют float64. PyTorch-мост отдельно показывает градиенты и работу с FP16: фаза вычисляется как минимум в FP32, результат возвращается в тип входа.
Запуск. Runtime → Run all не требует файлов репозитория, ключей API или GPU. Отсутствующие NumPy, Matplotlib и PyTorch устанавливаются первой исполняемой ячейкой; только эта установка требует сети. Дальнейший расчёт автономен. Встроенный SHA-256 проверяет целостность эталона, а математическую корректность проверяют отдельные сравнения.
0. Численный эталон
Данные принадлежат М4 и не зависят от формулировок лекции. Компактный JSON встроен в ноутбук. Его хеш позволяет обнаружить изменение данных; он сам по себе не доказывает правильность вычислений.
import importlib.util
import subprocess
import sys
for package in ("numpy", "matplotlib", "torch"):
if importlib.util.find_spec(package) is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package])
import hashlib
import json
import math
from dataclasses import dataclass
from numbers import Integral, Real
from typing import Any, Iterable, Sequence
import numpy as np
import matplotlib.pyplot as plt
np.set_printoptions(precision=6, suppress=True)
rng = np.random.default_rng(42)
REFERENCE_JSON = '{"alibi":{"allowed_bias_with_null_for_future":[[[-0.0,null,null,null],[-1.0,-0.0,null,null],[-2.0,-1.0,-0.0,null],[-3.0,-2.0,-1.0,-0.0]],[[-0.0,null,null,null],[-0.5,-0.0,null,null],[-1.0,-0.5,-0.0,null],[-1.5,-1.0,-0.5,-0.0]]],"positions":[0,1,2,3],"slopes":[1.0,0.5]},"meta":{"description":"Frozen numerical contract independent of lecture prose and figures","module":"module-04","version":2},"mrope":{"axis_sections":[2,1,1],"base":10000.0,"head_dim":8,"image_coordinates":[[0,0,0],[0,0,1],[0,1,0],[0,1,1]],"image_input":[[0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7],[0.8,0.9,1.0,1.1,1.2,1.3,1.4,1.5],[1.6,1.7,1.8,1.9,2.0,2.1,2.2,2.3],[2.4,2.5,2.6,2.7,2.8,2.9,3.0,3.1]],"image_output":[[0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7],[0.8,0.9,1.0,1.1,1.2,1.3,1.3984993002500583,1.5013992497667292],[1.6,1.7,1.8,1.9,1.9789003508315806,2.1198946675433303,2.2,2.3],[2.4,2.5,2.6,2.7,2.7708604844975793,2.9278545345439957,2.996898500516792,3.1029984495001295]],"text_coordinates":[[4,4,4],[5,5,5],[6,6,6]],"text_input":[[0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7],[0.8,0.9,1.0,1.1,1.2,1.3,1.4,1.5],[1.6,1.7,1.8,1.9,2.0,2.1,2.2,2.3]],"text_matches_standard_1d":true,"text_output":[[0.07568024953079283,-0.06536436208636119,0.0673866961079819,0.3542019666625956,0.37968537557107407,0.5155957870051426,0.5971952074730607,0.7023943936074717],[1.0899615955674056,-0.5118434528136071,0.3502144694257494,1.444766356683613,1.1335273924220777,1.3583503416382703,1.3924825312864193,1.5069812208724322],[2.0112788055787596,1.185224690187341,0.4127834073868535,2.5844941204394525,1.8704766662635748,2.216149146822818,2.1861604829186514,2.3131585209243424]],"text_positions":[4,5,6],"text_standard_rope_output":[[0.07568024953079283,-0.06536436208636119,0.0673866961079819,0.3542019666625956,0.37968537557107407,0.5155957870051426,0.5971952074730607,0.7023943936074717],[1.0899615955674056,-0.5118434528136071,0.3502144694257494,1.444766356683613,1.1335273924220777,1.3583503416382703,1.3924825312864193,1.5069812208724322],[2.0112788055787596,1.185224690187341,0.4127834073868535,2.5844941204394525,1.8704766662635748,2.216149146822818,2.1861604829186514,2.3131585209243424]]},"scaling":{"base":150000.0,"beta_fast":32.0,"beta_slow":1.0,"extended_context":131072,"factor":32.0,"head_dim":64,"ntk":[1.0,0.6161605983892874,0.37965388300744474,0.23392776373468369,0.14413707088263056,0.08881158384512078,0.05472219864590999,0.033717662662841354,0.020775495202624464,0.01280104155588286,0.007887497425078919,0.00485996513323059,0.0029945190246424332,0.001845104634111787,0.0011368807754451658,0.0007005011388955706,0.00043162120091427207,0.00026594797743284077,0.00016386666491543985,0.00010096818231035427,6.221261563062656e-05,3.83329624743296e-05,2.3619261096217023e-05,1.4553258050557898e-05,8.967144188945469e-06,5.525200929303662e-06,3.4044111108207913e-06,2.0976639872064776e-06,1.2924978975768017e-06,7.963862779878181e-07,4.907018455939914e-07,3.0235114281192144e-07],"ntk_base":5367776.967415848,"original_context":4096,"position_interpolation":[0.03125,0.0215326345590051,0.014836939233653148,0.01022330849575739,0.007044312506339714,0.0048538434213901555,0.0033445131711816074,0.0023045177565706506,0.001587914837983171,0.0010941436773476293,0.0007539134706999197,0.0005194797841181623,0.00035794458727063204,0.00024663967968231775,0.00016994566689118156,0.0001171000940817352,8.068715304598785e-05,5.55970233646647e-05,3.8308812373753384e-05,2.639646903147278e-05,1.818833668168956e-05,1.2532569824095001e-05,8.63549587543848e-06,5.950239261491605e-06,4.09997848180298e-06,2.8250668271503398e-06,1.9465962110014815e-06,1.3412910350540445e-06,9.242089502428302e-07,6.36820914615699e-07,4.387978250864396e-07,3.0235114281192144e-07],"raw":[1.0,0.6890443058881632,0.4747820554769007,0.3271458718642365,0.22541800020287084,0.15532298948448497,0.10702442147781144,0.07374456821026082,0.050813274815461475,0.03501259767512414,0.02412523106239743,0.016623353091781194,0.011454226792660225,0.007892469749834168,0.00543826134051781,0.0037472030106155264,0.0025819888974716113,0.0017791047476692703,0.0012258819959601083,0.0008446870090071289,0.0005820267738140659,0.00040104223437104005,0.00027633586801403137,0.00019040765636773135,0.00013119931141769537,9.040213846881087e-05,6.229107875204741e-05,4.292131312172942e-05,2.9574686407770566e-05,2.0378269267702367e-05,1.4041530402766067e-05,9.675236569981486e-06],"yarn":{"attention_factor":1.3465735902799727,"hf_truncated_high":18,"hf_truncated_low":8,"high":17.39802450158856,"interpolated_pairs":14,"inverse_frequencies":[1.0,0.6890443058881632,0.4747820554769007,0.3271458718642365,0.22541800020287084,0.15532298948448497,0.10702442147781144,0.07374456821026082,0.050813274815461475,0.03170569618466377,0.01933500112654036,0.011592049256286924,0.006794959489732219,0.0038603593171920685,0.0020937923789696887,0.0010526021013863357,0.00045648391922324086,0.0001293187012450632,3.8308812373753384e-05,2.639646903147278e-05,1.818833668168956e-05,1.2532569824095001e-05,8.63549587543848e-06,5.950239261491605e-06,4.09997848180298e-06,2.8250668271503398e-06,1.9465962110014815e-06,1.3412910350540445e-06,9.242089502428302e-07,6.36820914615699e-07,4.387978250864396e-07,3.0235114281192144e-07],"kept_pairs":9,"low":8.092779115512402,"ramp":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.09749564324709936,0.20496191184183662,0.3124281804365739,0.4198944490313112,0.5273607176260484,0.6348269862207857,0.742293254815523,0.8497595234102603,0.9572257920049976,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"score_factor_if_applied_to_q_and_k":1.8132604340394958,"transition_pairs":9,"truncate":false}},"sinusoidal":{"base":10000.0,"encoding":[[0.0,1.0,0.0,1.0,0.0,1.0,0.0,1.0],[0.8414709848078965,0.5403023058681398,0.09983341664682815,0.9950041652780258,0.009999833334166664,0.9999500004166653,0.0009999998333333417,0.9999995000000417],[0.9092974268256817,-0.4161468365471424,0.19866933079506122,0.9800665778412416,0.01999866669333308,0.9998000066665778,0.0019999986666669333,0.9999980000006666],[0.1411200080598672,-0.9899924966004454,0.2955202066613396,0.955336489125606,0.02999550020249566,0.9995500337489875,0.002999995500002025,0.999995500003375]],"model_dim":8,"positions":[0,1,2,3]},"spectrum":{"bases":{"10000":{"full_turn_counts":{"131072":32,"4096":23},"inverse_frequencies":[1.0,0.7498942093324558,0.5623413251903491,0.4216965034285822,0.31622776601683794,0.23713737056616552,0.1778279410038923,0.1333521432163324,0.1,0.07498942093324558,0.056234132519034905,0.042169650342858224,0.03162277660168379,0.023713737056616554,0.01778279410038923,0.01333521432163324,0.01,0.007498942093324558,0.005623413251903491,0.004216965034285822,0.0031622776601683794,0.0023713737056616554,0.0017782794100389228,0.001333521432163324,0.001,0.0007498942093324559,0.0005623413251903491,0.00042169650342858224,0.00031622776601683794,0.0002371373705661655,0.00017782794100389227,0.0001333521432163324],"maximum_wavelength_tokens":47117.2427801674,"minimum_wavelength_tokens":6.283185307179586,"wavelengths":[6.283185307179586,8.378762269377678,11.173259061216543,14.899780425245321,19.869176531592203,26.49597274431474,35.33294752055899,47.117242780167395,62.83185307179586,83.78762269377677,111.73259061216544,148.99780425245322,198.69176531592203,264.95972744314736,353.32947520558986,471.1724278016739,628.3185307179587,837.8762269377677,1117.3259061216543,1489.9780425245322,1986.9176531592202,2649.597274431474,3533.294752055899,4711.724278016739,6283.185307179586,8378.762269377676,11173.259061216542,14899.78042524532,19869.176531592202,26495.972744314742,35332.94752055899,47117.2427801674]},"150000":{"full_turn_counts":{"131072":27,"4096":18},"inverse_frequencies":[1.0,0.6890443058881632,0.4747820554769007,0.3271458718642365,0.22541800020287084,0.15532298948448497,0.10702442147781144,0.07374456821026082,0.050813274815461475,0.03501259767512414,0.02412523106239743,0.016623353091781194,0.011454226792660225,0.007892469749834168,0.00543826134051781,0.0037472030106155264,0.0025819888974716113,0.0017791047476692703,0.0012258819959601083,0.0008446870090071289,0.0005820267738140659,0.00040104223437104005,0.00027633586801403137,0.00019040765636773135,0.00013119931141769537,9.040213846881087e-05,6.229107875204741e-05,4.292131312172942e-05,2.9574686407770566e-05,2.0378269267702367e-05,1.4041530402766067e-05,9.675236569981486e-06],"maximum_wavelength_tokens":649408.9588128396,"minimum_wavelength_tokens":6.283185307179586,"wavelengths":[6.283185307179586,9.118695638999144,13.233830627546281,19.206066307286523,27.873485265262175,40.452384595695705,58.70795861748462,85.20200822472704,123.65243787176136,179.4549883296344,260.4404198628719,377.9734011837886,548.5473110420513,796.0987506238595,1155.3665618028806,1676.7667215733509,2433.467205584167,3531.6556348977883,5125.4405627016395,7438.477495427609,10795.354423311823,15667.141185350692,22737.494601535214,32998.59589177952,47890.3832595279,69502.61811945203,100868.14088081702,146388.46880941882,212451.46002727243,308327.72031027393,447471.54526274785,649408.9588128396]},"500000":{"full_turn_counts":{"131072":25,"4096":16},"inverse_frequencies":[1.0,0.6636012376960885,0.44036660267178046,0.2922278225730151,0.19392274474868576,0.12868737343265052,0.08539710028576561,0.05666962144529105,0.03760603093086393,0.024955408670558694,0.016560440080994446,0.010989528534539826,0.007292664737217109,0.004839421345719893,0.003211445994752591,0.0021311195369119653,0.001414213562373095,0.0009384738703573802,0.000622772421914596,0.0004132725499855165,0.0002742481756762073,0.00018199142881462546,0.00012076973741146504,8.01429472224798e-05,5.318295896944988e-05,3.529227739646723e-05,2.341999896140934e-05,1.5541540297632344e-05,1.031338537721246e-05,6.8439753011549275e-06,4.5416704806078695e-06,3.013858152139171e-06],"maximum_wavelength_tokens":2084764.7732591254,"minimum_wavelength_tokens":6.283185307179586,"wavelengths":[6.283185307179586,9.468314629722128,14.268078616903308,21.500982527458316,32.40045573469106,48.8251888245115,73.57609668424418,110.87395939719457,167.07919319459117,251.77649423117703,379.40931982782695,571.7429357803371,861.5760539647758,1298.3340069648193,1956.497265545218,2948.3026167007256,4442.882938158366,6695.109481084312,10089.055144515105,15203.490547339246,22910.58196353461,34524.62211052572,52026.15689866611,78399.72854675821,118142.83050307268,178032.86641424094,268282.9028956321,404283.3069857812,609226.2702664398,918060.7805699259,1383452.483839961,2084764.7732591254]}},"contexts":[4096,131072],"head_dim":64},"worked_rope":{"base":10000.0,"head_dim":4,"k":[0.0,1.0,1.0,-0.5],"logits":[1.1442484284723262,1.1442484284723262,1.1442484284723264],"placements":[[3,1],[7,5],[2,0]],"q":[1.0,0.0,0.5,0.5],"rotated_k_at_1":[-0.8414709848078965,0.5403023058681398,1.0049499170837486,-0.489975166874166],"rotated_q_at_3":[-0.9899924966004454,0.1411200080598672,0.48477726677324595,0.5147727669757416]}}'
REFERENCE_SHA256 = '88813060dd14b01e5a13f3dfe2736dfd6b07c38649bca8e0b28200d6bca8ceb3'
reference = json.loads(REFERENCE_JSON)
actual_sha256 = hashlib.sha256(
json.dumps(reference, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
assert actual_sha256 == REFERENCE_SHA256
print('Встроенный контракт М4 проверен', actual_sha256)1. Синусоидальные позиции и длины волн
Частота пары измеряется в радианах на шаг позиции. Длина волны — число таких шагов за один оборот. В таблице [T,D] синус находится на чётных координатах, косинус — на нечётных.
Посмотрите на медленные пары: увеличение базы сильнее меняет именно их. Число оборотов здесь равно на интервале длины L; у токенов с индексами 0…L−1 расстояние между крайними позициями равно L−1.
def _positive_int(name: str, value: object) -> int:
"""Return a strictly positive integer and reject booleans explicitly."""
if isinstance(value, bool) or not isinstance(value, Integral):
raise TypeError(f"{name} must be an integer")
result = int(value)
if result <= 0:
raise ValueError(f"{name} must be positive")
return result
def _positive_even_int(name: str, value: object, *, minimum: int = 2) -> int:
"""Return an even integer not smaller than ``minimum``."""
result = _positive_int(name, value)
if result < minimum:
raise ValueError(f"{name} must be at least {minimum}")
if result % 2:
raise ValueError(f"{name} must be even")
return result
def _finite_real(name: str, value: object) -> float:
"""Return a finite real scalar and reject booleans and complex values."""
if isinstance(value, bool) or not isinstance(value, Real):
raise TypeError(f"{name} must be a real scalar")
result = float(value)
if not math.isfinite(result):
raise ValueError(f"{name} must be finite")
return result
def _real_greater_than(name: str, value: object, lower: float) -> float:
"""Return a finite scalar strictly greater than ``lower``."""
result = _finite_real(name, value)
if result <= lower:
raise ValueError(f"{name} must be greater than {lower}")
return result
def _finite_array(
name: str,
value: object,
*,
ndim: int | None = None,
minimum_ndim: int | None = None,
) -> np.ndarray:
"""Convert to float64 and enforce rank, non-emptiness, and finite values."""
try:
if np.iscomplexobj(value):
raise TypeError(f"{name} must be real")
array = np.asarray(value, dtype=np.float64)
except (TypeError, ValueError) as exc:
raise TypeError(f"{name} must be numeric") from exc
if ndim is not None and array.ndim != ndim:
raise ValueError(f"{name} must have rank {ndim}, got shape {array.shape}")
if minimum_ndim is not None and array.ndim < minimum_ndim:
raise ValueError(f"{name} must have rank at least {minimum_ndim}, got shape {array.shape}")
if array.size == 0:
raise ValueError(f"{name} must be non-empty")
if not np.all(np.isfinite(array)):
raise ValueError(f"{name} must contain only finite values")
return array
def _position_vector(name: str, value: object) -> np.ndarray:
"""Validate a non-empty one-dimensional vector of non-negative integer positions."""
raw = np.asarray(value)
if raw.ndim != 1 or raw.size == 0:
raise ValueError(f"{name} must be a non-empty one-dimensional vector")
if raw.dtype.kind not in "iu":
raise TypeError(f"{name} must contain integer positions")
result = raw.astype(np.int64, copy=False)
if np.any(result < 0):
raise ValueError(f"{name} must contain non-negative positions")
return result
def _inverse_frequency_vector(value: object, *, expected_pairs: int | None = None) -> np.ndarray:
"""Validate a positive one-dimensional inverse-frequency vector."""
inv = _finite_array("inverse_frequencies", value, ndim=1)
if np.any(inv <= 0.0):
raise ValueError("inverse_frequencies must be strictly positive")
if expected_pairs is not None and inv.size != expected_pairs:
raise ValueError(
f"inverse_frequencies must have {expected_pairs} entries, got {inv.size}"
)
return inv
def _sequence_tensor(name: str, value: object) -> np.ndarray:
"""Validate a finite sequence tensor with shape ``[..., sequence, head_dim]``."""
tensor = _finite_array(name, value, minimum_ndim=2)
_positive_even_int(f"{name}.shape[-1]", tensor.shape[-1])
if tensor.shape[-2] <= 0:
raise ValueError(f"{name} must have a non-empty sequence axis")
return tensor
def rope_inverse_frequencies(head_dim: int, base: Real = 10_000.0) -> np.ndarray:
"""Return ``base**(-2i/head_dim)`` for the ``head_dim/2`` RoPE pairs."""
dim = _positive_even_int("head_dim", head_dim)
rope_base = _real_greater_than("base", base, 1.0)
pair_indices = np.arange(dim // 2, dtype=np.float64)
inv = rope_base ** (-2.0 * pair_indices / dim)
if not np.all(np.isfinite(inv)) or np.any(inv <= 0.0):
raise FloatingPointError("inverse-frequency construction produced invalid values")
return inv
def sinusoidal_encoding(
positions: object,
model_dim: int,
*,
base: Real = 10_000.0,
) -> np.ndarray:
"""Return the classic sinusoidal encoding with shape ``[positions, model_dim]``."""
pos = _position_vector("positions", positions)
dim = _positive_even_int("model_dim", model_dim)
inv = rope_inverse_frequencies(dim, base)
angles = pos.astype(np.float64)[:, None] * inv[None, :]
result = np.empty((pos.size, dim), dtype=np.float64)
result[:, 0::2] = np.sin(angles)
result[:, 1::2] = np.cos(angles)
if not np.all(np.isfinite(result)):
raise FloatingPointError("sinusoidal encoding produced non-finite values")
return result
def wavelengths_from_inverse_frequencies(inverse_frequencies: object) -> np.ndarray:
"""Convert radians-per-position frequencies to tokens per full ``2π`` turn."""
inv = _inverse_frequency_vector(inverse_frequencies)
return (2.0 * math.pi) / inv
def rotations_within_context(
inverse_frequencies: object,
context_length: int,
) -> np.ndarray:
"""Return turns (including fractions) across a span L = context_length.
This is the YaRN L*theta/(2*pi) diagnostic. Positions 0..L-1 have a maximum
endpoint separation of L-1, which is a different convention.
"""
inv = _inverse_frequency_vector(inverse_frequencies)
length = _positive_int("context_length", context_length)
return length * inv / (2.0 * math.pi)
def full_turn_count(inverse_frequencies: object, context_length: int) -> int:
"""Count frequency pairs that complete at least one full turn in the context."""
return int(np.count_nonzero(rotations_within_context(inverse_frequencies, context_length) >= 1.0))
positions = np.array(reference["sinusoidal"]["positions"], dtype=np.int64)
table = sinusoidal_encoding(
positions,
reference["sinusoidal"]["model_dim"],
base=reference["sinusoidal"]["base"],
)
assert table.shape == (4, 8)
assert np.allclose(table, np.array(reference["sinusoidal"]["encoding"]), atol=1e-12)
assert np.allclose(table[:, 0] ** 2 + table[:, 1] ** 2, 1.0, atol=1e-12)
for base, expected in reference["spectrum"]["bases"].items():
inv = rope_inverse_frequencies(reference["spectrum"]["head_dim"], float(base))
wave = wavelengths_from_inverse_frequencies(inv)
assert np.allclose(wave, np.array(expected["wavelengths"]), atol=1e-9)
for context, count in expected["full_turn_counts"].items():
assert full_turn_count(inv, int(context)) == count
print('Синусоидальная таблица и спектр проверены')2. Три вычисления одного вращения
Сначала сравним поворот соседних пар с умножением комплексных чисел. Затем переставим координаты и сравним с раскладкой split-half, где первая половина вектора спаривается со второй.
Без согласованной перестановки получится другой оператор. Отрицательный контроль ниже намеренно смешивает раскладки и показывает расхождение. Норма вектора сохраняется: это свойство чистого вращения, без дополнительного масштаба YaRN.
def rotate_half_split(x: object) -> np.ndarray:
"""Apply the split-half quarter turn used by HF/Llama-style RoPE implementations."""
values = _sequence_tensor("x", x)
half = values.shape[-1] // 2
return np.concatenate((-values[..., half:], values[..., :half]), axis=-1)
def _angles_for_sequence(
x: np.ndarray,
positions: np.ndarray,
inverse_frequencies: np.ndarray,
) -> np.ndarray:
"""Build a broadcastable angle tensor for ``[..., T, D]`` sequence tensors."""
if positions.size != x.shape[-2]:
raise ValueError(
f"positions length {positions.size} does not match sequence axis {x.shape[-2]}"
)
if inverse_frequencies.size != x.shape[-1] // 2:
raise ValueError("inverse_frequencies do not match head_dim")
base_angles = positions.astype(np.float64)[:, None] * inverse_frequencies[None, :]
return base_angles.reshape((1,) * (x.ndim - 2) + base_angles.shape)
def apply_rope_adjacent(
x: object,
positions: object,
inverse_frequencies: object,
) -> np.ndarray:
"""Apply RoPE to adjacent pairs ``(x_0,x_1), (x_2,x_3), ...``."""
values = _sequence_tensor("x", x)
pos = _position_vector("positions", positions)
inv = _inverse_frequency_vector(
inverse_frequencies,
expected_pairs=values.shape[-1] // 2,
)
angles = _angles_for_sequence(values, pos, inv)
cosine = np.cos(angles)
sine = np.sin(angles)
first = values[..., 0::2]
second = values[..., 1::2]
result = np.empty_like(values, dtype=np.float64)
result[..., 0::2] = cosine * first - sine * second
result[..., 1::2] = sine * first + cosine * second
if not np.all(np.isfinite(result)):
raise FloatingPointError("adjacent-pair RoPE produced non-finite values")
return result
def apply_rope_split_half(
x: object,
positions: object,
inverse_frequencies: object,
) -> np.ndarray:
"""Apply split-half RoPE to pairs ``(x_j, x_{j+D/2})``."""
values = _sequence_tensor("x", x)
pos = _position_vector("positions", positions)
inv = _inverse_frequency_vector(
inverse_frequencies,
expected_pairs=values.shape[-1] // 2,
)
angles = _angles_for_sequence(values, pos, inv)
repeated = np.concatenate((angles, angles), axis=-1)
result = values * np.cos(repeated) + rotate_half_split(values) * np.sin(repeated)
if not np.all(np.isfinite(result)):
raise FloatingPointError("split-half RoPE produced non-finite values")
return result
def apply_rope_complex_adjacent(
x: object,
positions: object,
inverse_frequencies: object,
) -> np.ndarray:
"""Apply adjacent-pair RoPE through the equivalent complex multiplication."""
values = _sequence_tensor("x", x)
pos = _position_vector("positions", positions)
inv = _inverse_frequency_vector(
inverse_frequencies,
expected_pairs=values.shape[-1] // 2,
)
angles = _angles_for_sequence(values, pos, inv)
complex_values = values[..., 0::2] + 1j * values[..., 1::2]
rotated = complex_values * np.exp(1j * angles)
result = np.empty_like(values, dtype=np.float64)
result[..., 0::2] = rotated.real
result[..., 1::2] = rotated.imag
if not np.all(np.isfinite(result)):
raise FloatingPointError("complex RoPE produced non-finite values")
return result
def adjacent_to_split_permutation(head_dim: int) -> np.ndarray:
"""Return the coordinate permutation mapping adjacent pairs to split-half pairs."""
dim = _positive_even_int("head_dim", head_dim)
return np.concatenate((np.arange(0, dim, 2), np.arange(1, dim, 2)))
def rope_attention_logits(
queries: object,
keys: object,
query_positions: object,
key_positions: object,
inverse_frequencies: object,
*,
pairing: str = "split_half",
) -> np.ndarray:
"""Return unscaled rotated dot products ``[..., query, key]``.
Divide by sqrt(head_dim) before the standard attention softmax. The historical
function name and fixture key 'logits' are retained for file compatibility.
"""
q = _sequence_tensor("queries", queries)
k = _sequence_tensor("keys", keys)
if q.ndim != k.ndim or q.shape[:-2] != k.shape[:-2]:
raise ValueError("queries and keys must have identical batch dimensions")
if q.shape[-1] != k.shape[-1]:
raise ValueError("queries and keys must have the same head_dim")
if pairing == "split_half":
apply = apply_rope_split_half
elif pairing == "adjacent":
apply = apply_rope_adjacent
else:
raise ValueError("pairing must be 'split_half' or 'adjacent'")
q_rot = apply(q, query_positions, inverse_frequencies)
k_rot = apply(k, key_positions, inverse_frequencies)
return np.matmul(q_rot, np.swapaxes(k_rot, -1, -2))
spec_inv = rope_inverse_frequencies(64, 150_000.0)
positions = np.array([0, 3, 17, 128, 4095], dtype=np.int64)
x = rng.standard_normal((2, 4, positions.size, 64))
adjacent = apply_rope_adjacent(x, positions, spec_inv)
complex_form = apply_rope_complex_adjacent(x, positions, spec_inv)
assert np.allclose(adjacent, complex_form, atol=1e-12)
assert np.allclose(np.linalg.norm(adjacent, axis=-1), np.linalg.norm(x, axis=-1), atol=1e-12)
perm = adjacent_to_split_permutation(64)
split = apply_rope_split_half(x[..., perm], positions, spec_inv)
assert np.allclose(split, adjacent[..., perm], atol=1e-12)
q = rng.standard_normal((1, 1, 64))
k = rng.standard_normal((1, 1, 64))
raw_adjacent = rope_attention_logits(q, k, [3], [1], spec_inv, pairing="adjacent").item()
raw_split = rope_attention_logits(q, k, [3], [1], spec_inv, pairing="split_half").item()
assert abs(raw_adjacent - raw_split) > 1e-3
q_perm, k_perm = q[..., perm], k[..., perm]
permuted_split = rope_attention_logits(q_perm, k_perm, [3], [1], spec_inv, pairing="split_half").item()
assert abs(raw_adjacent - permuted_split) < 1e-10
print('Три реализации, перестановка и нормы проверены', f"raw convention gap={abs(raw_adjacent-raw_split):.6f}")3. Общий сдвиг и точность позиции
При фиксированных q, k и частотах общий сдвиг c не меняет скалярное произведение:
Проверим тождество на 200 примерах и воспроизведём число из лекции. Функция rope_attention_logits возвращает немасштабированное произведение. Для стандартного внимания его нужно разделить на √D. При D=4 получаем 1.144248/2 ≈ 0.572124.
Целые позиции переводятся в float64 только при вычислении фазы. FP32 не различает все соседние целые после 2²⁴: пример около 10⁸ показывает коллизию. Поэтому сохранение ID целыми до вычисления фазы не отменяет ограничений выбранного вещественного типа.
max_error = 0.0
for _ in range(200):
query = rng.standard_normal((1, 1, 64))
key = rng.standard_normal((1, 1, 64))
m, n, shift = (int(value) for value in rng.integers(0, 100_000, size=3))
first = rope_attention_logits(query, key, [m], [n], spec_inv, pairing="split_half").item()
second = rope_attention_logits(
query, key, [m + shift], [n + shift], spec_inv, pairing="split_half"
).item()
max_error = max(max_error, abs(first - second))
assert max_error < 1e-9
worked = reference["worked_rope"]
q4 = np.array(worked["q"], dtype=np.float64)[None, :]
k4 = np.array(worked["k"], dtype=np.float64)[None, :]
inv4 = rope_inverse_frequencies(worked["head_dim"], worked["base"])
observed = []
for q_pos, k_pos in worked["placements"]:
observed.append(
float(rope_attention_logits(q4, k4, [q_pos], [k_pos], inv4, pairing="adjacent").item())
)
assert np.allclose(observed, worked["logits"], atol=1e-12)
assert np.float32(100_000_000) == np.float32(100_000_001)
large_positions = np.array([100_000_000, 100_000_001], dtype=np.int64)
large_x = np.ones((2, 4), dtype=np.float64)
large_rotated = apply_rope_adjacent(large_x, large_positions, inv4)
assert not np.allclose(large_rotated[0], large_rotated[1], atol=0.0, rtol=0.0)
print('Общий сдвиг и большие позиции проверены', f"max_error={max_error:.3e}")4. Три преобразования спектра
PI делит каждую частоту на s. Статическая NTK-aware эвристика меняет базу, сохраняя первую частоту и деля последнюю на s. YaRN смешивает исходную и интерполированную частоты с весом, зависящим от индекса пары.
Проверьте обе границы рампы, численность трёх зон и отличие непрерывных границ от режима truncate=True. Группы пар совпадают в этом примере, но частоты внутри перехода различаются. Масштаб a для Q и K даёт множитель a² для их скалярного произведения, если масштабируется вся голова.
def position_interpolated_frequencies(
inverse_frequencies: object,
factor: Real,
) -> np.ndarray:
"""Apply position interpolation by dividing every frequency by ``factor``."""
inv = _inverse_frequency_vector(inverse_frequencies)
scale = _finite_real("factor", factor)
if scale < 1.0:
raise ValueError("factor must be at least 1")
return inv / scale
def ntk_scaled_base(head_dim: int, base: Real, factor: Real) -> float:
"""Return the static NTK-aware base ``base * factor**(d/(d-2))``."""
dim = _positive_even_int("head_dim", head_dim, minimum=4)
rope_base = _real_greater_than("base", base, 1.0)
scale = _finite_real("factor", factor)
if scale < 1.0:
raise ValueError("factor must be at least 1")
result = rope_base * scale ** (dim / (dim - 2))
if not math.isfinite(result):
raise FloatingPointError("NTK base is non-finite")
return result
def ntk_scaled_frequencies(head_dim: int, base: Real, factor: Real) -> np.ndarray:
"""Return inverse frequencies generated from the static NTK-aware base."""
return rope_inverse_frequencies(head_dim, ntk_scaled_base(head_dim, base, factor))
@dataclass(frozen=True)
class YarnSpectrum:
"""Piecewise YaRN spectrum plus diagnostic boundaries and scaling metadata."""
inverse_frequencies: np.ndarray
original_inverse_frequencies: np.ndarray
ramp: np.ndarray
low: float
high: float
truncate: bool
kept_pairs: int
transition_pairs: int
interpolated_pairs: int
attention_factor: float
score_factor_if_applied_to_q_and_k: float
def yarn_scaled_frequencies(
head_dim: int,
base: Real,
factor: Real,
original_context: int,
*,
beta_fast: Real = 32.0,
beta_slow: Real = 1.0,
truncate: bool = False,
) -> YarnSpectrum:
"""Construct the lecture's piecewise YaRN frequency spectrum.
``ramp == 0`` keeps a frequency, ``ramp == 1`` divides it by ``factor``.
With ``truncate=False`` this follows the continuous correction bounds used by the
original gpt-oss implementation. ``truncate=True`` reproduces the integer-boundary
option supported by the generic Hugging Face YaRN utility.
"""
dim = _positive_even_int("head_dim", head_dim, minimum=4)
rope_base = _real_greater_than("base", base, 1.0)
scale = _finite_real("factor", factor)
if scale < 1.0:
raise ValueError("factor must be at least 1")
context = _positive_int("original_context", original_context)
fast = _real_greater_than("beta_fast", beta_fast, 0.0)
slow = _real_greater_than("beta_slow", beta_slow, 0.0)
if fast <= slow:
raise ValueError("beta_fast must be greater than beta_slow")
if not isinstance(truncate, bool):
raise TypeError("truncate must be a bool")
original = rope_inverse_frequencies(dim, rope_base)
pair_count = dim // 2
def correction_dimension(rotations: float) -> float:
"""Map a target turn count to the corresponding continuous pair index."""
return dim * math.log(context / (rotations * 2.0 * math.pi)) / (2.0 * math.log(rope_base))
low_raw = correction_dimension(fast)
high_raw = correction_dimension(slow)
low = math.floor(low_raw) if truncate else low_raw
high = math.ceil(high_raw) if truncate else high_raw
low = max(0.0, min(float(pair_count - 1), float(low)))
high = max(0.0, min(float(pair_count - 1), float(high)))
if high <= low:
raise ValueError("YaRN correction range is empty for this configuration")
pair_indices = np.arange(pair_count, dtype=np.float64)
ramp = np.clip((pair_indices - low) / (high - low), 0.0, 1.0)
scaled = original * (1.0 - ramp) + (original / scale) * ramp
attention_factor = 0.1 * math.log(scale) + 1.0 if scale > 1.0 else 1.0
score_factor_if_applied_to_q_and_k = attention_factor**2
kept = int(np.count_nonzero(ramp == 0.0))
interpolated = int(np.count_nonzero(ramp == 1.0))
transition = pair_count - kept - interpolated
return YarnSpectrum(
inverse_frequencies=scaled,
original_inverse_frequencies=original,
ramp=ramp,
low=low,
high=high,
truncate=truncate,
kept_pairs=kept,
transition_pairs=transition,
interpolated_pairs=interpolated,
attention_factor=attention_factor,
score_factor_if_applied_to_q_and_k=score_factor_if_applied_to_q_and_k,
)
scaling = reference["scaling"]
raw = np.array(scaling["raw"], dtype=np.float64)
pi = position_interpolated_frequencies(raw, scaling["factor"])
ntk = ntk_scaled_frequencies(
scaling["head_dim"], scaling["base"], scaling["factor"]
)
yarn = yarn_scaled_frequencies(
scaling["head_dim"],
scaling["base"],
scaling["factor"],
scaling["original_context"],
beta_fast=32.0,
beta_slow=1.0,
)
assert np.allclose(pi, scaling["position_interpolation"], atol=1e-12)
assert np.allclose(ntk, scaling["ntk"], atol=1e-12)
assert math.isclose(
ntk_scaled_base(scaling["head_dim"], scaling["base"], scaling["factor"]),
scaling["ntk_base"],
rel_tol=1e-12,
)
# PI can be read either as position/factor or inverse_frequency/factor.
probe_positions = np.array([0, 1, 17, 4096, 131071], dtype=np.int64)
angles_by_position = np.outer(probe_positions.astype(np.float64) / scaling["factor"], raw)
angles_by_frequency = np.outer(probe_positions.astype(np.float64), pi)
assert np.allclose(angles_by_position, angles_by_frequency, atol=1e-15)
assert np.allclose(yarn.inverse_frequencies, scaling["yarn"]["inverse_frequencies"], atol=1e-12)
assert abs(yarn.low - 8.092779115512402) < 1e-12
assert abs(yarn.high - 17.39802450158856) < 1e-12
assert yarn.truncate is False
assert (yarn.kept_pairs, yarn.transition_pairs, yarn.interpolated_pairs) == (9, 9, 14)
yarn_truncated = yarn_scaled_frequencies(
scaling["head_dim"], scaling["base"], scaling["factor"], scaling["original_context"],
beta_fast=scaling["beta_fast"], beta_slow=scaling["beta_slow"], truncate=True,
)
assert (yarn_truncated.low, yarn_truncated.high) == (8.0, 18.0)
assert abs(yarn.attention_factor - 1.34657359028) < 1e-12
assert abs(yarn.score_factor_if_applied_to_q_and_k - 1.81326043404) < 1e-12
# In this declared scenario, the chosen beta boundaries happen to make the fully
# interpolated pairs coincide with pairs below one full turn. This is a checked
# property of the scenario, not a general theorem about extrapolation.
no_turn = np.flatnonzero(rotations_within_context(raw, scaling["original_context"]) < 1.0)
fully_interpolated = np.flatnonzero(yarn.ramp == 1.0)
assert np.array_equal(no_turn, fully_interpolated)
assert np.allclose(position_interpolated_frequencies(raw, 1.0), raw)
print(
'PI/NTK-aware/YaRN и масштабы проверены',
f"bounds={yarn.low:.4f}/{yarn.high:.4f}; "
f"zones={yarn.kept_pairs}/{yarn.transition_pairs}/{yarn.interpolated_pairs}",
)5. Прочитайте график
Верхняя панель сравнивает частоты, нижняя показывает вес интерполяции YaRN. Найдите неизменную первую пару NTK и полностью интерполированную часть YaRN. Это геометрия преобразования; график не измеряет качество модели. Он строится той же функцией, что иллюстрация лекции, и не сохраняет файлы.
from matplotlib.colors import LinearSegmentedColormap
INK = '#2E3440'
MUTED = '#4C566A'
BORDER = '#D8DEE9'
BLUE = '#5E81AC'
ORANGE = '#D08770'
PURPLE = '#B48EAD'
GREEN = '#A3BE8C'
PALE = {'blue': '#E8EEF5', 'green': '#EFF4EA', 'purple': '#F3EEF3', 'orange': '#FAF0E8', 'yellow': '#FAF5E3', 'gray': '#ECEFF4'}
def plot_style():
import matplotlib.pyplot as plt
plt.rcParams.update({
"font.family": "DejaVu Sans", "font.size": 12,
"text.color": INK, "axes.labelcolor": INK, "axes.titlecolor": INK,
"axes.titlesize": 17, "axes.titleweight": "bold", "axes.titlelocation": "left",
"axes.labelsize": 13, "xtick.labelsize": 11, "ytick.labelsize": 11,
"xtick.color": MUTED, "ytick.color": MUTED,
"axes.edgecolor": BORDER, "axes.linewidth": .7,
"axes.facecolor": "white", "figure.facecolor": "white", "savefig.facecolor": "white",
"grid.color": BORDER, "grid.linewidth": .7, "axes.axisbelow": True,
"legend.fontsize": 11, "legend.frameon": False, "savefig.dpi": 150,
"xtick.major.size": 0, "ytick.major.size": 0,
})
def scaling_plot(reference, lang='en'):
plot_style()
ru = lang == 'ru'; sc = reference['scaling']; yr = sc['yarn']; x = np.arange(len(sc['raw']))
fig, (ax, ramp_ax) = plt.subplots(2, 1, figsize=(11, 7), sharex=True,
height_ratios=[3, 1], layout='constrained')
series = [('Исходные' if ru else 'Original', sc['raw'], MUTED, '-'),
('PI', sc['position_interpolation'], PURPLE, '--'),
('Статическая NTK-aware' if ru else 'Static NTK-aware', sc['ntk'], ORANGE, '-.'),
('YaRN', yr['inverse_frequencies'], BLUE, '-')]
for name, values, color, style in series:
ax.plot(x, values, linestyle=style, color=color, linewidth=2.2, label=name)
ax.set_yscale('log'); ax.grid(axis='y')
ax.set_ylabel('Частота, рад/шаг (лог. шкала)' if ru else 'Frequency, rad/step (log scale)')
ax.set_title('Как меняется спектр · s=32' if ru else 'How the spectrum changes · s=32', pad=16)
ax.legend(loc='lower left', ncol=2)
ramp_ax.plot(x, yr['ramp'], color=BLUE, marker='o', markersize=3)
for value in (yr['low'], yr['high']):
ramp_ax.axvline(value, color=MUTED, linestyle=':', linewidth=1.4)
ramp_ax.set(xlabel='Индекс пары i' if ru else 'Pair index i',
ylabel='Вес PI' if ru else 'PI weight', ylim=(-.08,1.08), yticks=[0,.5,1])
ramp_ax.grid(axis='y')
return fig
scaling_plot(reference, 'ru')
plt.show()6. Смещение ALiBi и секционная M-RoPE
ALiBi возвращает смещения [H,T_q,T_k]: для разрешённых ключей они равны −наклон × расстояние. Будущие позиции получают −∞. В полном attention смещение прибавляется к QKᵀ/√D.
Для M-RoPE зададим координаты [T,A], где A — число осей, и количество частотных пар для каждой оси. В нашем примере это время, высота и ширина. Текст повторяет одну позицию по всем осям, поэтому сводится к обычной RoPE с тем же спектром. Это секционная раскладка; чередование осей Qwen3-VL обсуждается в лекции отдельно.
def alibi_bias(
slopes: object,
query_positions: object,
key_positions: object,
*,
causal: bool = True,
) -> np.ndarray:
"""Return an ALiBi tensor with shape ``[heads, query, key]``.
In causal mode, future keys receive ``-inf`` and allowed keys receive
``-slope * (query_position - key_position)``. In bidirectional mode the absolute
distance is used.
"""
if not isinstance(causal, bool):
raise TypeError("causal must be a bool")
slope_values = _finite_array("slopes", slopes, ndim=1)
if np.any(slope_values <= 0.0):
raise ValueError("slopes must be strictly positive")
q_pos = _position_vector("query_positions", query_positions)
k_pos = _position_vector("key_positions", key_positions)
delta = q_pos[:, None] - k_pos[None, :]
if causal:
allowed = delta >= 0
base = -np.abs(delta).astype(np.float64)
bias = slope_values[:, None, None] * base[None, :, :]
bias = np.where(allowed[None, :, :], bias, -np.inf)
else:
bias = -slope_values[:, None, None] * np.abs(delta)[None, :, :]
return bias
def apply_mrope_adjacent(
x: object,
coordinates: object,
axis_sections: Sequence[int],
inverse_frequencies: object,
) -> np.ndarray:
"""Apply adjacent-pair multi-axis RoPE.
``coordinates`` has shape ``[sequence, axes]``. ``axis_sections`` gives the number of
frequency pairs assigned to each axis and must sum to ``head_dim / 2``.
"""
values = _sequence_tensor("x", x)
raw_coordinates = np.asarray(coordinates)
if raw_coordinates.ndim != 2 or raw_coordinates.shape[0] != values.shape[-2]:
raise ValueError("coordinates must have shape [sequence, axes]")
if raw_coordinates.dtype.kind not in "iu":
raise TypeError("coordinates must contain integer positions")
coords = raw_coordinates.astype(np.int64, copy=False)
if np.any(coords < 0):
raise ValueError("coordinates must be non-negative")
if isinstance(axis_sections, (str, bytes)):
raise TypeError("axis_sections must be a sequence of integers")
sections: list[int] = []
for index, value in enumerate(axis_sections):
sections.append(_positive_int(f"axis_sections[{index}]", value))
if len(sections) != coords.shape[1]:
raise ValueError("axis_sections length must match the coordinate axis count")
pair_count = values.shape[-1] // 2
if sum(sections) != pair_count:
raise ValueError("axis_sections must sum to head_dim / 2")
inv = _inverse_frequency_vector(inverse_frequencies, expected_pairs=pair_count)
axis_index = np.repeat(np.arange(len(sections)), sections)
base_angles = coords[:, axis_index].astype(np.float64) * inv[None, :]
angles = base_angles.reshape((1,) * (values.ndim - 2) + base_angles.shape)
cosine = np.cos(angles)
sine = np.sin(angles)
first = values[..., 0::2]
second = values[..., 1::2]
result = np.empty_like(values, dtype=np.float64)
result[..., 0::2] = cosine * first - sine * second
result[..., 1::2] = sine * first + cosine * second
if not np.all(np.isfinite(result)):
raise FloatingPointError("M-RoPE produced non-finite values")
return result
slopes = np.array(reference["alibi"]["slopes"], dtype=np.float64)
bias = alibi_bias(slopes, np.arange(4), np.arange(4), causal=True)
assert bias.shape == (2, 4, 4)
assert np.allclose(np.diagonal(bias, axis1=1, axis2=2), 0.0)
future_mask = np.triu(np.ones((4, 4), dtype=bool), k=1)
assert np.all(np.isneginf(bias[0][future_mask]))
assert np.all(np.isfinite(bias[0][~future_mask]))
rectangular_bias = alibi_bias(slopes, np.array([4, 5, 6]), np.arange(7), causal=True)
assert rectangular_bias.shape == (2, 3, 7)
assert np.all(np.isneginf(rectangular_bias[:, 0, 5:]))
assert np.all(np.isfinite(rectangular_bias[:, 2, :]))
mref = reference["mrope"]
m_inv = rope_inverse_frequencies(mref["head_dim"], mref["base"])
image_input = np.array(mref["image_input"], dtype=np.float64)
image_coordinates = np.array(mref["image_coordinates"], dtype=np.int64)
image_output = apply_mrope_adjacent(
image_input, image_coordinates, mref["axis_sections"], m_inv
)
assert np.allclose(image_output, mref["image_output"], atol=1e-12)
assert np.all(image_coordinates[:, 0] == image_coordinates[0, 0])
assert np.allclose(
np.linalg.norm(image_output, axis=-1), np.linalg.norm(image_input, axis=-1), atol=1e-12
)
text_input = np.array(mref["text_input"], dtype=np.float64)
text_positions = np.array(mref["text_positions"], dtype=np.int64)
text_coordinates = np.array(mref["text_coordinates"], dtype=np.int64)
text_output = apply_mrope_adjacent(
text_input, text_coordinates, mref["axis_sections"], m_inv
)
text_1d = apply_rope_adjacent(text_input, text_positions, m_inv)
assert np.all(text_coordinates == text_positions[:, None])
assert np.allclose(text_output, mref["text_output"], atol=1e-12)
assert np.allclose(text_output, text_1d, atol=1e-12)
query = rng.standard_normal((1, 8))
key = rng.standard_normal((1, 8))
q_coord = np.array([[5, 1, 2]], dtype=np.int64)
k_coord = np.array([[8, 3, 1]], dtype=np.int64)
shift = np.array([[11, 4, 9]], dtype=np.int64)
first = float(
apply_mrope_adjacent(query, q_coord, (2, 1, 1), m_inv)[0]
@ apply_mrope_adjacent(key, k_coord, (2, 1, 1), m_inv)[0]
)
second = float(
apply_mrope_adjacent(query, q_coord + shift, (2, 1, 1), m_inv)[0]
@ apply_mrope_adjacent(key, k_coord + shift, (2, 1, 1), m_inv)[0]
)
assert abs(first - second) < 1e-12
print('Геометрия ALiBi и M-RoPE проверена')7. Некорректный вход должен быть заметен
Проверим нечётную ширину, несовместимые формы, нецелые позиции, NaN, неверную базу, коэффициент расширения и параметры рампы. Каждая проверка ожидает определённый класс исключения. Это позволяет отличить осмысленный отказ от случайного сбоя позднее в вычислении.
def expect_error(expected_exception, function, *args, **kwargs):
"""Assert that one invalid call fails with the declared exception type."""
try:
function(*args, **kwargs)
except expected_exception:
return
raise AssertionError(f"{function.__name__} did not raise {expected_exception.__name__}")
expect_error(ValueError, rope_inverse_frequencies, 63, 10_000.0)
expect_error(ValueError, rope_inverse_frequencies, 64, 1.0)
expect_error(ValueError, apply_rope_adjacent, np.ones((2, 8)), [0], np.ones(4))
expect_error(ValueError, apply_rope_adjacent, np.array([[np.nan] * 8]), [0], np.ones(4))
expect_error(ValueError, rope_attention_logits, np.ones((1, 1, 8)), np.ones((1, 1, 6)), [0], [0], np.ones(4))
expect_error(ValueError, rope_attention_logits, np.ones((1, 1, 8)), np.ones((1, 1, 8)), [0], [0], np.ones(4), pairing="unknown")
expect_error(ValueError, position_interpolated_frequencies, np.ones(4), 0.5)
expect_error(ValueError, ntk_scaled_base, 2, 10_000.0, 2.0)
expect_error(ValueError, yarn_scaled_frequencies, 64, 150_000.0, 32.0, 4096, beta_fast=1.0, beta_slow=32.0)
expect_error(ValueError, alibi_bias, np.array([-0.5]), np.arange(2), np.arange(2))
expect_error(TypeError, sinusoidal_encoding, np.array([0.0, 1.0]), 8)
expect_error(ValueError, apply_mrope_adjacent, np.ones((4, 8)), np.ones((4, 3), dtype=int), (1, 1, 1), np.ones(4))
expect_error(TypeError, apply_mrope_adjacent, np.ones((4, 8)), np.ones((4, 3), dtype=float), (2, 1, 1), np.ones(4))
print('Отрицательные тесты пройдены')8. Проверка кэша и частичного масштабирования
Сравним полный каузальный расчёт с добавлением одного токена за шаг. Новые запросы сохраняют абсолютные позиции, а старые ключи не вращаются повторно. Два отрицательных контроля сбрасывают позиции запросов и смешивают старый и новый спектры.
Вторая проверка меняет только координату ширины изображения: измениться должна лишь выделенная ей пара. Затем проверяем границу правила a²: если масштабируется только часть головы, весь логит не обязан получать этот множитель.
"""Behavioral checks embedded in the M4 notebook after its NumPy core."""
import numpy as np
def check_cached_positions():
"""Compare full causal attention with cached steps; require wrong IDs to fail."""
rng = np.random.default_rng(404)
q, k, v = [rng.standard_normal((7, 8)) for _ in range(3)]
pos = np.arange(7, dtype=np.int64)
inv = rope_inverse_frequencies(8, 10_000.0)
qr = apply_rope_adjacent(q, pos, inv)
kr = apply_rope_adjacent(k, pos, inv)
def attend(queries, keys, values, allowed):
scores = (queries @ keys.T) / np.sqrt(queries.shape[-1])
scores = np.where(allowed, scores, -np.inf)
weights = np.exp(scores - scores.max(axis=-1, keepdims=True))
weights /= weights.sum(axis=-1, keepdims=True)
return weights @ values
full = attend(qr, kr, v, np.tril(np.ones((7, 7), dtype=bool)))
cache_k, cache_v, steps, wrong = [], [], [], []
for t in range(7):
cache_k.append(apply_rope_adjacent(k[t:t+1], [t], inv))
cache_v.append(v[t:t+1])
keys, values = np.concatenate(cache_k), np.concatenate(cache_v)
query = apply_rope_adjacent(q[t:t+1], [t], inv)
steps.append(attend(query, keys, values, np.ones((1, t+1), dtype=bool)))
reset = apply_rope_adjacent(q[t:t+1], [0], inv)
wrong.append(attend(reset, keys, values, np.ones((1, t+1), dtype=bool)))
cached = np.concatenate(steps)
np.testing.assert_allclose(cached, full, rtol=1e-12, atol=1e-12)
wrong_gap = float(np.max(np.abs(np.concatenate(wrong)-full)))
assert wrong_gap > .01, 'The negative control must expose the reset-position bug'
# Changing frequencies for Q alone leaves cached K in a different geometry.
new_inv = inv / 4
q_new = apply_rope_adjacent(q[-1:], [6], new_inv)
new_keys = apply_rope_adjacent(k, pos, new_inv)
mixed = attend(q_new, kr, v, np.ones((1, 7), dtype=bool))
consistent = attend(q_new, new_keys, v, np.ones((1, 7), dtype=bool))
spectrum_gap = float(np.max(np.abs(mixed-consistent)))
assert spectrum_gap > .01
return {'full_vs_cached_max_error': float(np.max(np.abs(cached-full))),
'reset_query_position_gap': wrong_gap, 'mixed_spectrum_gap': spectrum_gap}
def check_axis_and_scaling_contracts():
"""Probe one M-RoPE axis and the full-versus-partial amplitude distinction."""
inv = rope_inverse_frequencies(8, 10_000.0)
x = np.ones((1, 8))
first = apply_mrope_adjacent(x, [[0, 1, 2]], (2, 1, 1), inv)
changed = apply_mrope_adjacent(x, [[0, 1, 3]], (2, 1, 1), inv)
np.testing.assert_array_equal(first[:, :6], changed[:, :6])
assert not np.allclose(first[:, 6:], changed[:, 6:])
q = np.array([1., 2., 3., 4.]); k = np.array([2., -1., 1., 2.]); a = 1.3
np.testing.assert_allclose((a*q) @ (a*k), a*a*(q@k), rtol=1e-14)
partial_q, partial_k = q.copy(), k.copy()
partial_q[2:] *= a; partial_k[2:] *= a
expected = q[:2]@k[:2] + a*a*(q[2:]@k[2:])
np.testing.assert_allclose(partial_q@partial_k, expected, rtol=1e-14)
# Nonzero unrotated contribution is essential for the counterexample.
k[0] = 3.; partial_k = k.copy(); partial_k[2:] *= a
assert not np.isclose(partial_q@partial_k, a*a*(q@k))
return {'only_width_pairs_changed': True, 'partial_rope_is_not_global_scaling': True}
print(check_cached_positions())
print(check_axis_and_scaling_contracts())Практические задания
-
Измените размер батча и число голов, сохранив
[B,H,T,D]. Объясните, почему поворот не смешивает эти оси. -
В проверке кэша замените абсолютную позицию на ноль. Найдите первый шаг, где появляются различия, и объясните, почему первый шаг может совпасть.
-
Постройте ALiBi для запросов на позициях 4, 5, 6 и ключей 0…6. Для запроса 4 должны быть разрешены ровно пять ключей.
-
Измените только
wв M-RoPE и укажите номера затронутых координат. Готовая проверка выше служит образцом: при секциях(2,1,1)это координаты 6 и 7. -
Расширение. Реализуйте Llama3-style scaling по закреплённой версии исходного кода и сравните частоты с PI/NTK/YaRN. Укажите границы длин волн и версию реализации.
-
Проект. Перенесите расчёт на выбранную открытую модель. Зафиксируйте ревизии конфигурации и кода; отдельно измерьте качество, память и время. Геометрические проверки этого ноутбука не заменяют оценку на длинных текстах.
PyTorch bridge · B02 — RoPE: NumPy и PyTorch
Эта ячейка встраивает канонический NumPy→PyTorch-мост и выполняет проверку эквивалентности. Код самодостаточен и не импортирует файлы репозитория.
import importlib.util
import subprocess
import sys
missing = []
if importlib.util.find_spec("numpy") is None:
missing.append("numpy>=1.26")
if importlib.util.find_spec("torch") is None:
missing.append("torch>=2.5")
if missing:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])
import numpy as np
import torch
import torch.nn.functional as F
import math
def numpy_rope_explicit(
x: np.ndarray,
position_ids: np.ndarray,
*,
base: float = 10_000.0,
) -> np.ndarray:
"""Adjacent pairs; valid floating x[..., T, D], integer IDs[T], base > 1."""
dim = x.shape[-1]
inv_freq = base ** (-np.arange(0, dim, 2) / dim)
angles = position_ids[:, None] * inv_freq[None, :]
shape = (1,) * (x.ndim - 2) + angles.shape
cos, sin = np.cos(angles).reshape(shape), np.sin(angles).reshape(shape)
even, odd = x[..., 0::2], x[..., 1::2]
out = np.empty_like(x, dtype=np.result_type(x.dtype, np.float64))
out[..., 0::2] = even * cos - odd * sin
out[..., 1::2] = even * sin + odd * cos
return out
def torch_rope_explicit(
x: torch.Tensor,
position_ids: torch.Tensor,
*,
base: float = 10_000.0,
) -> torch.Tensor:
"""Adjacent pairs with at least float32 phase arithmetic; output keeps x.dtype."""
dim = x.shape[-1]
work_dtype = torch.float64 if x.dtype == torch.float64 else torch.float32
inv_freq = base ** (-torch.arange(0, dim, 2, device=x.device, dtype=work_dtype) / dim)
angles = position_ids.to(device=x.device, dtype=work_dtype)[:, None] * inv_freq[None, :]
shape = (1,) * (x.ndim - 2) + tuple(angles.shape)
cos, sin = angles.cos().reshape(shape), angles.sin().reshape(shape)
even, odd = x[..., 0::2].to(work_dtype), x[..., 1::2].to(work_dtype)
out = torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
return out.to(x.dtype)
rng = np.random.default_rng(2)
x_np = rng.standard_normal((2, 5, 8))
pos_np = np.arange(5, dtype=np.int64) + 11
np_out = numpy_rope_explicit(x_np, pos_np)
x = torch.tensor(x_np, dtype=torch.float64, requires_grad=True)
t_out = torch_rope_explicit(x, torch.tensor(pos_np))
np.testing.assert_allclose(np_out, t_out.detach().numpy(), rtol=1e-12, atol=1e-12)
t_out.square().sum().backward()
# A rotation is orthogonal: d ||R x||^2 / dx = 2 x.
torch.testing.assert_close(x.grad, 2 * x.detach(), rtol=1e-12, atol=1e-12)
# Half-precision IDs would merge 2048 and 2049 before the rotation.
long_ids = torch.tensor([2048, 2049], dtype=torch.int64)
half_x = torch.ones((2, 8), dtype=torch.float16)
half_out = torch_rope_explicit(half_x, long_ids)
expected = numpy_rope_explicit(half_x.numpy(), long_ids.numpy())
np.testing.assert_allclose(half_out.numpy(), expected, rtol=1e-3, atol=1e-3)
assert not torch.equal(half_out[0], half_out[1])
assert long_ids.to(torch.float16)[0] == long_ids.to(torch.float16)[1]
print("B02 outputs, analytic gradient, and float16 positions: PASS")Демо проектных упражнений модуля 4 — Спектр по конфигурации и поворот по нескольким осям
Проектная демонстрация
Скачать .ipynb module_04_project_demo.ipynb · 56,3 КБ
Показать текст ноутбука
Демо проектных упражнений модуля 4 — Спектр по конфигурации и поворот по нескольким осям
Демо-реализация, не готовое решение полномасштабного проекта. Точные PI/статическая NTK/YaRN и секционная M-RoPE на малых входах. Это расчёт геометрии, без обучения и измерения качества модели. Конфигурация gpt-oss закреплена ревизией и хешем. Все входы малы, детерминированы и работают без сети на CPU. Цель — проверить интерфейсы, метрики, инварианты и структуру отчёта до расходования GPU/API-бюджета.
from __future__ import annotations
import importlib.util, subprocess, sys
for package in ("numpy", "matplotlib"):
if importlib.util.find_spec(package) is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package])
import math
import random
import time
from collections import Counter, defaultdict
import matplotlib.pyplot as plt
import numpy as np
SEED = 20260801
random.seed(SEED)
np.random.seed(SEED)
print(f"воспроизводимый тестовый запуск; seed={SEED}")
DEMO_LANGUAGE = 'ru'
Демо 1. Восстановить спектр из конфигурации
Можно ли по конфигурации воспроизвести частоты, не загружая веса модели? Начнём с сохранённых байтов config.json: проверим хеш и прочитаем ширину головы, базу, опорную длину и коэффициент расширения. Функции ниже последовательно строят исходную геометрию, PI, статическую NTK-эвристику и рампу YaRN. Они повторяют реализации основного ноутбука; блоки ALiBi и M-RoPE понадобятся во втором опыте.
При чтении результата найдите неизменную первую пару NTK и переходную зону YaRN. Сравните масштаб вращаемых Q/K с множителем их скалярного произведения.
import importlib.util
import subprocess
import sys
for package in ('numpy', 'matplotlib'):
if importlib.util.find_spec(package) is None:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', package])
import hashlib
import json
from dataclasses import dataclass
from numbers import Integral, Real
from typing import Sequence
def _positive_int(name: str, value: object) -> int:
"""Return a strictly positive integer and reject booleans explicitly."""
if isinstance(value, bool) or not isinstance(value, Integral):
raise TypeError(f"{name} must be an integer")
result = int(value)
if result <= 0:
raise ValueError(f"{name} must be positive")
return result
def _positive_even_int(name: str, value: object, *, minimum: int = 2) -> int:
"""Return an even integer not smaller than ``minimum``."""
result = _positive_int(name, value)
if result < minimum:
raise ValueError(f"{name} must be at least {minimum}")
if result % 2:
raise ValueError(f"{name} must be even")
return result
def _finite_real(name: str, value: object) -> float:
"""Return a finite real scalar and reject booleans and complex values."""
if isinstance(value, bool) or not isinstance(value, Real):
raise TypeError(f"{name} must be a real scalar")
result = float(value)
if not math.isfinite(result):
raise ValueError(f"{name} must be finite")
return result
def _real_greater_than(name: str, value: object, lower: float) -> float:
"""Return a finite scalar strictly greater than ``lower``."""
result = _finite_real(name, value)
if result <= lower:
raise ValueError(f"{name} must be greater than {lower}")
return result
def _finite_array(
name: str,
value: object,
*,
ndim: int | None = None,
minimum_ndim: int | None = None,
) -> np.ndarray:
"""Convert to float64 and enforce rank, non-emptiness, and finite values."""
try:
if np.iscomplexobj(value):
raise TypeError(f"{name} must be real")
array = np.asarray(value, dtype=np.float64)
except (TypeError, ValueError) as exc:
raise TypeError(f"{name} must be numeric") from exc
if ndim is not None and array.ndim != ndim:
raise ValueError(f"{name} must have rank {ndim}, got shape {array.shape}")
if minimum_ndim is not None and array.ndim < minimum_ndim:
raise ValueError(f"{name} must have rank at least {minimum_ndim}, got shape {array.shape}")
if array.size == 0:
raise ValueError(f"{name} must be non-empty")
if not np.all(np.isfinite(array)):
raise ValueError(f"{name} must contain only finite values")
return array
def _position_vector(name: str, value: object) -> np.ndarray:
"""Validate a non-empty one-dimensional vector of non-negative integer positions."""
raw = np.asarray(value)
if raw.ndim != 1 or raw.size == 0:
raise ValueError(f"{name} must be a non-empty one-dimensional vector")
if raw.dtype.kind not in "iu":
raise TypeError(f"{name} must contain integer positions")
result = raw.astype(np.int64, copy=False)
if np.any(result < 0):
raise ValueError(f"{name} must contain non-negative positions")
return result
def _inverse_frequency_vector(value: object, *, expected_pairs: int | None = None) -> np.ndarray:
"""Validate a positive one-dimensional inverse-frequency vector."""
inv = _finite_array("inverse_frequencies", value, ndim=1)
if np.any(inv <= 0.0):
raise ValueError("inverse_frequencies must be strictly positive")
if expected_pairs is not None and inv.size != expected_pairs:
raise ValueError(
f"inverse_frequencies must have {expected_pairs} entries, got {inv.size}"
)
return inv
def _sequence_tensor(name: str, value: object) -> np.ndarray:
"""Validate a finite sequence tensor with shape ``[..., sequence, head_dim]``."""
tensor = _finite_array(name, value, minimum_ndim=2)
_positive_even_int(f"{name}.shape[-1]", tensor.shape[-1])
if tensor.shape[-2] <= 0:
raise ValueError(f"{name} must have a non-empty sequence axis")
return tensor
def rope_inverse_frequencies(head_dim: int, base: Real = 10_000.0) -> np.ndarray:
"""Return ``base**(-2i/head_dim)`` for the ``head_dim/2`` RoPE pairs."""
dim = _positive_even_int("head_dim", head_dim)
rope_base = _real_greater_than("base", base, 1.0)
pair_indices = np.arange(dim // 2, dtype=np.float64)
inv = rope_base ** (-2.0 * pair_indices / dim)
if not np.all(np.isfinite(inv)) or np.any(inv <= 0.0):
raise FloatingPointError("inverse-frequency construction produced invalid values")
return inv
def sinusoidal_encoding(
positions: object,
model_dim: int,
*,
base: Real = 10_000.0,
) -> np.ndarray:
"""Return the classic sinusoidal encoding with shape ``[positions, model_dim]``."""
pos = _position_vector("positions", positions)
dim = _positive_even_int("model_dim", model_dim)
inv = rope_inverse_frequencies(dim, base)
angles = pos.astype(np.float64)[:, None] * inv[None, :]
result = np.empty((pos.size, dim), dtype=np.float64)
result[:, 0::2] = np.sin(angles)
result[:, 1::2] = np.cos(angles)
if not np.all(np.isfinite(result)):
raise FloatingPointError("sinusoidal encoding produced non-finite values")
return result
def wavelengths_from_inverse_frequencies(inverse_frequencies: object) -> np.ndarray:
"""Convert radians-per-position frequencies to tokens per full ``2π`` turn."""
inv = _inverse_frequency_vector(inverse_frequencies)
return (2.0 * math.pi) / inv
def rotations_within_context(
inverse_frequencies: object,
context_length: int,
) -> np.ndarray:
"""Return turns (including fractions) across a span L = context_length.
This is the YaRN L*theta/(2*pi) diagnostic. Positions 0..L-1 have a maximum
endpoint separation of L-1, which is a different convention.
"""
inv = _inverse_frequency_vector(inverse_frequencies)
length = _positive_int("context_length", context_length)
return length * inv / (2.0 * math.pi)
def full_turn_count(inverse_frequencies: object, context_length: int) -> int:
"""Count frequency pairs that complete at least one full turn in the context."""
return int(np.count_nonzero(rotations_within_context(inverse_frequencies, context_length) >= 1.0))
def rotate_half_split(x: object) -> np.ndarray:
"""Apply the split-half quarter turn used by HF/Llama-style RoPE implementations."""
values = _sequence_tensor("x", x)
half = values.shape[-1] // 2
return np.concatenate((-values[..., half:], values[..., :half]), axis=-1)
def _angles_for_sequence(
x: np.ndarray,
positions: np.ndarray,
inverse_frequencies: np.ndarray,
) -> np.ndarray:
"""Build a broadcastable angle tensor for ``[..., T, D]`` sequence tensors."""
if positions.size != x.shape[-2]:
raise ValueError(
f"positions length {positions.size} does not match sequence axis {x.shape[-2]}"
)
if inverse_frequencies.size != x.shape[-1] // 2:
raise ValueError("inverse_frequencies do not match head_dim")
base_angles = positions.astype(np.float64)[:, None] * inverse_frequencies[None, :]
return base_angles.reshape((1,) * (x.ndim - 2) + base_angles.shape)
def apply_rope_adjacent(
x: object,
positions: object,
inverse_frequencies: object,
) -> np.ndarray:
"""Apply RoPE to adjacent pairs ``(x_0,x_1), (x_2,x_3), ...``."""
values = _sequence_tensor("x", x)
pos = _position_vector("positions", positions)
inv = _inverse_frequency_vector(
inverse_frequencies,
expected_pairs=values.shape[-1] // 2,
)
angles = _angles_for_sequence(values, pos, inv)
cosine = np.cos(angles)
sine = np.sin(angles)
first = values[..., 0::2]
second = values[..., 1::2]
result = np.empty_like(values, dtype=np.float64)
result[..., 0::2] = cosine * first - sine * second
result[..., 1::2] = sine * first + cosine * second
if not np.all(np.isfinite(result)):
raise FloatingPointError("adjacent-pair RoPE produced non-finite values")
return result
def apply_rope_split_half(
x: object,
positions: object,
inverse_frequencies: object,
) -> np.ndarray:
"""Apply split-half RoPE to pairs ``(x_j, x_{j+D/2})``."""
values = _sequence_tensor("x", x)
pos = _position_vector("positions", positions)
inv = _inverse_frequency_vector(
inverse_frequencies,
expected_pairs=values.shape[-1] // 2,
)
angles = _angles_for_sequence(values, pos, inv)
repeated = np.concatenate((angles, angles), axis=-1)
result = values * np.cos(repeated) + rotate_half_split(values) * np.sin(repeated)
if not np.all(np.isfinite(result)):
raise FloatingPointError("split-half RoPE produced non-finite values")
return result
def apply_rope_complex_adjacent(
x: object,
positions: object,
inverse_frequencies: object,
) -> np.ndarray:
"""Apply adjacent-pair RoPE through the equivalent complex multiplication."""
values = _sequence_tensor("x", x)
pos = _position_vector("positions", positions)
inv = _inverse_frequency_vector(
inverse_frequencies,
expected_pairs=values.shape[-1] // 2,
)
angles = _angles_for_sequence(values, pos, inv)
complex_values = values[..., 0::2] + 1j * values[..., 1::2]
rotated = complex_values * np.exp(1j * angles)
result = np.empty_like(values, dtype=np.float64)
result[..., 0::2] = rotated.real
result[..., 1::2] = rotated.imag
if not np.all(np.isfinite(result)):
raise FloatingPointError("complex RoPE produced non-finite values")
return result
def adjacent_to_split_permutation(head_dim: int) -> np.ndarray:
"""Return the coordinate permutation mapping adjacent pairs to split-half pairs."""
dim = _positive_even_int("head_dim", head_dim)
return np.concatenate((np.arange(0, dim, 2), np.arange(1, dim, 2)))
def rope_attention_logits(
queries: object,
keys: object,
query_positions: object,
key_positions: object,
inverse_frequencies: object,
*,
pairing: str = "split_half",
) -> np.ndarray:
"""Return unscaled rotated dot products ``[..., query, key]``.
Divide by sqrt(head_dim) before the standard attention softmax. The historical
function name and fixture key 'logits' are retained for file compatibility.
"""
q = _sequence_tensor("queries", queries)
k = _sequence_tensor("keys", keys)
if q.ndim != k.ndim or q.shape[:-2] != k.shape[:-2]:
raise ValueError("queries and keys must have identical batch dimensions")
if q.shape[-1] != k.shape[-1]:
raise ValueError("queries and keys must have the same head_dim")
if pairing == "split_half":
apply = apply_rope_split_half
elif pairing == "adjacent":
apply = apply_rope_adjacent
else:
raise ValueError("pairing must be 'split_half' or 'adjacent'")
q_rot = apply(q, query_positions, inverse_frequencies)
k_rot = apply(k, key_positions, inverse_frequencies)
return np.matmul(q_rot, np.swapaxes(k_rot, -1, -2))
def position_interpolated_frequencies(
inverse_frequencies: object,
factor: Real,
) -> np.ndarray:
"""Apply position interpolation by dividing every frequency by ``factor``."""
inv = _inverse_frequency_vector(inverse_frequencies)
scale = _finite_real("factor", factor)
if scale < 1.0:
raise ValueError("factor must be at least 1")
return inv / scale
def ntk_scaled_base(head_dim: int, base: Real, factor: Real) -> float:
"""Return the static NTK-aware base ``base * factor**(d/(d-2))``."""
dim = _positive_even_int("head_dim", head_dim, minimum=4)
rope_base = _real_greater_than("base", base, 1.0)
scale = _finite_real("factor", factor)
if scale < 1.0:
raise ValueError("factor must be at least 1")
result = rope_base * scale ** (dim / (dim - 2))
if not math.isfinite(result):
raise FloatingPointError("NTK base is non-finite")
return result
def ntk_scaled_frequencies(head_dim: int, base: Real, factor: Real) -> np.ndarray:
"""Return inverse frequencies generated from the static NTK-aware base."""
return rope_inverse_frequencies(head_dim, ntk_scaled_base(head_dim, base, factor))
@dataclass(frozen=True)
class YarnSpectrum:
"""Piecewise YaRN spectrum plus diagnostic boundaries and scaling metadata."""
inverse_frequencies: np.ndarray
original_inverse_frequencies: np.ndarray
ramp: np.ndarray
low: float
high: float
truncate: bool
kept_pairs: int
transition_pairs: int
interpolated_pairs: int
attention_factor: float
score_factor_if_applied_to_q_and_k: float
def yarn_scaled_frequencies(
head_dim: int,
base: Real,
factor: Real,
original_context: int,
*,
beta_fast: Real = 32.0,
beta_slow: Real = 1.0,
truncate: bool = False,
) -> YarnSpectrum:
"""Construct the lecture's piecewise YaRN frequency spectrum.
``ramp == 0`` keeps a frequency, ``ramp == 1`` divides it by ``factor``.
With ``truncate=False`` this follows the continuous correction bounds used by the
original gpt-oss implementation. ``truncate=True`` reproduces the integer-boundary
option supported by the generic Hugging Face YaRN utility.
"""
dim = _positive_even_int("head_dim", head_dim, minimum=4)
rope_base = _real_greater_than("base", base, 1.0)
scale = _finite_real("factor", factor)
if scale < 1.0:
raise ValueError("factor must be at least 1")
context = _positive_int("original_context", original_context)
fast = _real_greater_than("beta_fast", beta_fast, 0.0)
slow = _real_greater_than("beta_slow", beta_slow, 0.0)
if fast <= slow:
raise ValueError("beta_fast must be greater than beta_slow")
if not isinstance(truncate, bool):
raise TypeError("truncate must be a bool")
original = rope_inverse_frequencies(dim, rope_base)
pair_count = dim // 2
def correction_dimension(rotations: float) -> float:
"""Map a target turn count to the corresponding continuous pair index."""
return dim * math.log(context / (rotations * 2.0 * math.pi)) / (2.0 * math.log(rope_base))
low_raw = correction_dimension(fast)
high_raw = correction_dimension(slow)
low = math.floor(low_raw) if truncate else low_raw
high = math.ceil(high_raw) if truncate else high_raw
low = max(0.0, min(float(pair_count - 1), float(low)))
high = max(0.0, min(float(pair_count - 1), float(high)))
if high <= low:
raise ValueError("YaRN correction range is empty for this configuration")
pair_indices = np.arange(pair_count, dtype=np.float64)
ramp = np.clip((pair_indices - low) / (high - low), 0.0, 1.0)
scaled = original * (1.0 - ramp) + (original / scale) * ramp
attention_factor = 0.1 * math.log(scale) + 1.0 if scale > 1.0 else 1.0
score_factor_if_applied_to_q_and_k = attention_factor**2
kept = int(np.count_nonzero(ramp == 0.0))
interpolated = int(np.count_nonzero(ramp == 1.0))
transition = pair_count - kept - interpolated
return YarnSpectrum(
inverse_frequencies=scaled,
original_inverse_frequencies=original,
ramp=ramp,
low=low,
high=high,
truncate=truncate,
kept_pairs=kept,
transition_pairs=transition,
interpolated_pairs=interpolated,
attention_factor=attention_factor,
score_factor_if_applied_to_q_and_k=score_factor_if_applied_to_q_and_k,
)
def alibi_bias(
slopes: object,
query_positions: object,
key_positions: object,
*,
causal: bool = True,
) -> np.ndarray:
"""Return an ALiBi tensor with shape ``[heads, query, key]``.
In causal mode, future keys receive ``-inf`` and allowed keys receive
``-slope * (query_position - key_position)``. In bidirectional mode the absolute
distance is used.
"""
if not isinstance(causal, bool):
raise TypeError("causal must be a bool")
slope_values = _finite_array("slopes", slopes, ndim=1)
if np.any(slope_values <= 0.0):
raise ValueError("slopes must be strictly positive")
q_pos = _position_vector("query_positions", query_positions)
k_pos = _position_vector("key_positions", key_positions)
delta = q_pos[:, None] - k_pos[None, :]
if causal:
allowed = delta >= 0
base = -np.abs(delta).astype(np.float64)
bias = slope_values[:, None, None] * base[None, :, :]
bias = np.where(allowed[None, :, :], bias, -np.inf)
else:
bias = -slope_values[:, None, None] * np.abs(delta)[None, :, :]
return bias
def apply_mrope_adjacent(
x: object,
coordinates: object,
axis_sections: Sequence[int],
inverse_frequencies: object,
) -> np.ndarray:
"""Apply adjacent-pair multi-axis RoPE.
``coordinates`` has shape ``[sequence, axes]``. ``axis_sections`` gives the number of
frequency pairs assigned to each axis and must sum to ``head_dim / 2``.
"""
values = _sequence_tensor("x", x)
raw_coordinates = np.asarray(coordinates)
if raw_coordinates.ndim != 2 or raw_coordinates.shape[0] != values.shape[-2]:
raise ValueError("coordinates must have shape [sequence, axes]")
if raw_coordinates.dtype.kind not in "iu":
raise TypeError("coordinates must contain integer positions")
coords = raw_coordinates.astype(np.int64, copy=False)
if np.any(coords < 0):
raise ValueError("coordinates must be non-negative")
if isinstance(axis_sections, (str, bytes)):
raise TypeError("axis_sections must be a sequence of integers")
sections: list[int] = []
for index, value in enumerate(axis_sections):
sections.append(_positive_int(f"axis_sections[{index}]", value))
if len(sections) != coords.shape[1]:
raise ValueError("axis_sections length must match the coordinate axis count")
pair_count = values.shape[-1] // 2
if sum(sections) != pair_count:
raise ValueError("axis_sections must sum to head_dim / 2")
inv = _inverse_frequency_vector(inverse_frequencies, expected_pairs=pair_count)
axis_index = np.repeat(np.arange(len(sections)), sections)
base_angles = coords[:, axis_index].astype(np.float64) * inv[None, :]
angles = base_angles.reshape((1,) * (values.ndim - 2) + base_angles.shape)
cosine = np.cos(angles)
sine = np.sin(angles)
first = values[..., 0::2]
second = values[..., 1::2]
result = np.empty_like(values, dtype=np.float64)
result[..., 0::2] = cosine * first - sine * second
result[..., 1::2] = sine * first + cosine * second
if not np.all(np.isfinite(result)):
raise FloatingPointError("M-RoPE produced non-finite values")
return result
INK = '#2E3440'
MUTED = '#4C566A'
BORDER = '#D8DEE9'
BLUE = '#5E81AC'
ORANGE = '#D08770'
PURPLE = '#B48EAD'
GREEN = '#A3BE8C'
PALE = {'blue': '#E8EEF5', 'green': '#EFF4EA', 'purple': '#F3EEF3', 'orange': '#FAF0E8', 'yellow': '#FAF5E3', 'gray': '#ECEFF4'}
def plot_style():
import matplotlib.pyplot as plt
plt.rcParams.update({
"font.family": "DejaVu Sans", "font.size": 12,
"text.color": INK, "axes.labelcolor": INK, "axes.titlecolor": INK,
"axes.titlesize": 17, "axes.titleweight": "bold", "axes.titlelocation": "left",
"axes.labelsize": 13, "xtick.labelsize": 11, "ytick.labelsize": 11,
"xtick.color": MUTED, "ytick.color": MUTED,
"axes.edgecolor": BORDER, "axes.linewidth": .7,
"axes.facecolor": "white", "figure.facecolor": "white", "savefig.facecolor": "white",
"grid.color": BORDER, "grid.linewidth": .7, "axes.axisbelow": True,
"legend.fontsize": 11, "legend.frameon": False, "savefig.dpi": 150,
"xtick.major.size": 0, "ytick.major.size": 0,
})
def scaling_plot(reference, lang='en'):
plot_style()
ru = lang == 'ru'; sc = reference['scaling']; yr = sc['yarn']; x = np.arange(len(sc['raw']))
fig, (ax, ramp_ax) = plt.subplots(2, 1, figsize=(11, 7), sharex=True,
height_ratios=[3, 1], layout='constrained')
series = [('Исходные' if ru else 'Original', sc['raw'], MUTED, '-'),
('PI', sc['position_interpolation'], PURPLE, '--'),
('Статическая NTK-aware' if ru else 'Static NTK-aware', sc['ntk'], ORANGE, '-.'),
('YaRN', yr['inverse_frequencies'], BLUE, '-')]
for name, values, color, style in series:
ax.plot(x, values, linestyle=style, color=color, linewidth=2.2, label=name)
ax.set_yscale('log'); ax.grid(axis='y')
ax.set_ylabel('Частота, рад/шаг (лог. шкала)' if ru else 'Frequency, rad/step (log scale)')
ax.set_title('Как меняется спектр · s=32' if ru else 'How the spectrum changes · s=32', pad=16)
ax.legend(loc='lower left', ncol=2)
ramp_ax.plot(x, yr['ramp'], color=BLUE, marker='o', markersize=3)
for value in (yr['low'], yr['high']):
ramp_ax.axvline(value, color=MUTED, linestyle=':', linewidth=1.4)
ramp_ax.set(xlabel='Индекс пары i' if ru else 'Pair index i',
ylabel='Вес PI' if ru else 'PI weight', ylim=(-.08,1.08), yticks=[0,.5,1])
ramp_ax.grid(axis='y')
return fig
reference = json.loads('{\n "alibi": {\n "allowed_bias_with_null_for_future": [\n [\n [\n -0.0,\n null,\n null,\n null\n ],\n [\n -1.0,\n -0.0,\n null,\n null\n ],\n [\n -2.0,\n -1.0,\n -0.0,\n null\n ],\n [\n -3.0,\n -2.0,\n -1.0,\n -0.0\n ]\n ],\n [\n [\n -0.0,\n null,\n null,\n null\n ],\n [\n -0.5,\n -0.0,\n null,\n null\n ],\n [\n -1.0,\n -0.5,\n -0.0,\n null\n ],\n [\n -1.5,\n -1.0,\n -0.5,\n -0.0\n ]\n ]\n ],\n "positions": [\n 0,\n 1,\n 2,\n 3\n ],\n "slopes": [\n 1.0,\n 0.5\n ]\n },\n "meta": {\n "description": "Frozen numerical contract independent of lecture prose and figures",\n "module": "module-04",\n "version": 2\n },\n "mrope": {\n "axis_sections": [\n 2,\n 1,\n 1\n ],\n "base": 10000.0,\n "head_dim": 8,\n "image_coordinates": [\n [\n 0,\n 0,\n 0\n ],\n [\n 0,\n 0,\n 1\n ],\n [\n 0,\n 1,\n 0\n ],\n [\n 0,\n 1,\n 1\n ]\n ],\n "image_input": [\n [\n 0.0,\n 0.1,\n 0.2,\n 0.3,\n 0.4,\n 0.5,\n 0.6,\n 0.7\n ],\n [\n 0.8,\n 0.9,\n 1.0,\n 1.1,\n 1.2,\n 1.3,\n 1.4,\n 1.5\n ],\n [\n 1.6,\n 1.7,\n 1.8,\n 1.9,\n 2.0,\n 2.1,\n 2.2,\n 2.3\n ],\n [\n 2.4,\n 2.5,\n 2.6,\n 2.7,\n 2.8,\n 2.9,\n 3.0,\n 3.1\n ]\n ],\n "image_output": [\n [\n 0.0,\n 0.1,\n 0.2,\n 0.3,\n 0.4,\n 0.5,\n 0.6,\n 0.7\n ],\n [\n 0.8,\n 0.9,\n 1.0,\n 1.1,\n 1.2,\n 1.3,\n 1.3984993002500583,\n 1.5013992497667292\n ],\n [\n 1.6,\n 1.7,\n 1.8,\n 1.9,\n 1.9789003508315806,\n 2.1198946675433303,\n 2.2,\n 2.3\n ],\n [\n 2.4,\n 2.5,\n 2.6,\n 2.7,\n 2.7708604844975793,\n 2.9278545345439957,\n 2.996898500516792,\n 3.1029984495001295\n ]\n ],\n "text_coordinates": [\n [\n 4,\n 4,\n 4\n ],\n [\n 5,\n 5,\n 5\n ],\n [\n 6,\n 6,\n 6\n ]\n ],\n "text_input": [\n [\n 0.0,\n 0.1,\n 0.2,\n 0.3,\n 0.4,\n 0.5,\n 0.6,\n 0.7\n ],\n [\n 0.8,\n 0.9,\n 1.0,\n 1.1,\n 1.2,\n 1.3,\n 1.4,\n 1.5\n ],\n [\n 1.6,\n 1.7,\n 1.8,\n 1.9,\n 2.0,\n 2.1,\n 2.2,\n 2.3\n ]\n ],\n "text_matches_standard_1d": true,\n "text_output": [\n [\n 0.07568024953079283,\n -0.06536436208636119,\n 0.0673866961079819,\n 0.3542019666625956,\n 0.37968537557107407,\n 0.5155957870051426,\n 0.5971952074730607,\n 0.7023943936074717\n ],\n [\n 1.0899615955674056,\n -0.5118434528136071,\n 0.3502144694257494,\n 1.444766356683613,\n 1.1335273924220777,\n 1.3583503416382703,\n 1.3924825312864193,\n 1.5069812208724322\n ],\n [\n 2.0112788055787596,\n 1.185224690187341,\n 0.4127834073868535,\n 2.5844941204394525,\n 1.8704766662635748,\n 2.216149146822818,\n 2.1861604829186514,\n 2.3131585209243424\n ]\n ],\n "text_positions": [\n 4,\n 5,\n 6\n ],\n "text_standard_rope_output": [\n [\n 0.07568024953079283,\n -0.06536436208636119,\n 0.0673866961079819,\n 0.3542019666625956,\n 0.37968537557107407,\n 0.5155957870051426,\n 0.5971952074730607,\n 0.7023943936074717\n ],\n [\n 1.0899615955674056,\n -0.5118434528136071,\n 0.3502144694257494,\n 1.444766356683613,\n 1.1335273924220777,\n 1.3583503416382703,\n 1.3924825312864193,\n 1.5069812208724322\n ],\n [\n 2.0112788055787596,\n 1.185224690187341,\n 0.4127834073868535,\n 2.5844941204394525,\n 1.8704766662635748,\n 2.216149146822818,\n 2.1861604829186514,\n 2.3131585209243424\n ]\n ]\n },\n "scaling": {\n "base": 150000.0,\n "beta_fast": 32.0,\n "beta_slow": 1.0,\n "extended_context": 131072,\n "factor": 32.0,\n "head_dim": 64,\n "ntk": [\n 1.0,\n 0.6161605983892874,\n 0.37965388300744474,\n 0.23392776373468369,\n 0.14413707088263056,\n 0.08881158384512078,\n 0.05472219864590999,\n 0.033717662662841354,\n 0.020775495202624464,\n 0.01280104155588286,\n 0.007887497425078919,\n 0.00485996513323059,\n 0.0029945190246424332,\n 0.001845104634111787,\n 0.0011368807754451658,\n 0.0007005011388955706,\n 0.00043162120091427207,\n 0.00026594797743284077,\n 0.00016386666491543985,\n 0.00010096818231035427,\n 6.221261563062656e-05,\n 3.83329624743296e-05,\n 2.3619261096217023e-05,\n 1.4553258050557898e-05,\n 8.967144188945469e-06,\n 5.525200929303662e-06,\n 3.4044111108207913e-06,\n 2.0976639872064776e-06,\n 1.2924978975768017e-06,\n 7.963862779878181e-07,\n 4.907018455939914e-07,\n 3.0235114281192144e-07\n ],\n "ntk_base": 5367776.967415848,\n "original_context": 4096,\n "position_interpolation": [\n 0.03125,\n 0.0215326345590051,\n 0.014836939233653148,\n 0.01022330849575739,\n 0.007044312506339714,\n 0.0048538434213901555,\n 0.0033445131711816074,\n 0.0023045177565706506,\n 0.001587914837983171,\n 0.0010941436773476293,\n 0.0007539134706999197,\n 0.0005194797841181623,\n 0.00035794458727063204,\n 0.00024663967968231775,\n 0.00016994566689118156,\n 0.0001171000940817352,\n 8.068715304598785e-05,\n 5.55970233646647e-05,\n 3.8308812373753384e-05,\n 2.639646903147278e-05,\n 1.818833668168956e-05,\n 1.2532569824095001e-05,\n 8.63549587543848e-06,\n 5.950239261491605e-06,\n 4.09997848180298e-06,\n 2.8250668271503398e-06,\n 1.9465962110014815e-06,\n 1.3412910350540445e-06,\n 9.242089502428302e-07,\n 6.36820914615699e-07,\n 4.387978250864396e-07,\n 3.0235114281192144e-07\n ],\n "raw": [\n 1.0,\n 0.6890443058881632,\n 0.4747820554769007,\n 0.3271458718642365,\n 0.22541800020287084,\n 0.15532298948448497,\n 0.10702442147781144,\n 0.07374456821026082,\n 0.050813274815461475,\n 0.03501259767512414,\n 0.02412523106239743,\n 0.016623353091781194,\n 0.011454226792660225,\n 0.007892469749834168,\n 0.00543826134051781,\n 0.0037472030106155264,\n 0.0025819888974716113,\n 0.0017791047476692703,\n 0.0012258819959601083,\n 0.0008446870090071289,\n 0.0005820267738140659,\n 0.00040104223437104005,\n 0.00027633586801403137,\n 0.00019040765636773135,\n 0.00013119931141769537,\n 9.040213846881087e-05,\n 6.229107875204741e-05,\n 4.292131312172942e-05,\n 2.9574686407770566e-05,\n 2.0378269267702367e-05,\n 1.4041530402766067e-05,\n 9.675236569981486e-06\n ],\n "yarn": {\n "attention_factor": 1.3465735902799727,\n "hf_truncated_high": 18,\n "hf_truncated_low": 8,\n "high": 17.39802450158856,\n "interpolated_pairs": 14,\n "inverse_frequencies": [\n 1.0,\n 0.6890443058881632,\n 0.4747820554769007,\n 0.3271458718642365,\n 0.22541800020287084,\n 0.15532298948448497,\n 0.10702442147781144,\n 0.07374456821026082,\n 0.050813274815461475,\n 0.03170569618466377,\n 0.01933500112654036,\n 0.011592049256286924,\n 0.006794959489732219,\n 0.0038603593171920685,\n 0.0020937923789696887,\n 0.0010526021013863357,\n 0.00045648391922324086,\n 0.0001293187012450632,\n 3.8308812373753384e-05,\n 2.639646903147278e-05,\n 1.818833668168956e-05,\n 1.2532569824095001e-05,\n 8.63549587543848e-06,\n 5.950239261491605e-06,\n 4.09997848180298e-06,\n 2.8250668271503398e-06,\n 1.9465962110014815e-06,\n 1.3412910350540445e-06,\n 9.242089502428302e-07,\n 6.36820914615699e-07,\n 4.387978250864396e-07,\n 3.0235114281192144e-07\n ],\n "kept_pairs": 9,\n "low": 8.092779115512402,\n "ramp": [\n 0.0,\n 0.0,\n 0.0,\n 0.0,\n 0.0,\n 0.0,\n 0.0,\n 0.0,\n 0.0,\n 0.09749564324709936,\n 0.20496191184183662,\n 0.3124281804365739,\n 0.4198944490313112,\n 0.5273607176260484,\n 0.6348269862207857,\n 0.742293254815523,\n 0.8497595234102603,\n 0.9572257920049976,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0,\n 1.0\n ],\n "score_factor_if_applied_to_q_and_k": 1.8132604340394958,\n "transition_pairs": 9,\n "truncate": false\n }\n },\n "sinusoidal": {\n "base": 10000.0,\n "encoding": [\n [\n 0.0,\n 1.0,\n 0.0,\n 1.0,\n 0.0,\n 1.0,\n 0.0,\n 1.0\n ],\n [\n 0.8414709848078965,\n 0.5403023058681398,\n 0.09983341664682815,\n 0.9950041652780258,\n 0.009999833334166664,\n 0.9999500004166653,\n 0.0009999998333333417,\n 0.9999995000000417\n ],\n [\n 0.9092974268256817,\n -0.4161468365471424,\n 0.19866933079506122,\n 0.9800665778412416,\n 0.01999866669333308,\n 0.9998000066665778,\n 0.0019999986666669333,\n 0.9999980000006666\n ],\n [\n 0.1411200080598672,\n -0.9899924966004454,\n 0.2955202066613396,\n 0.955336489125606,\n 0.02999550020249566,\n 0.9995500337489875,\n 0.002999995500002025,\n 0.999995500003375\n ]\n ],\n "model_dim": 8,\n "positions": [\n 0,\n 1,\n 2,\n 3\n ]\n },\n "spectrum": {\n "bases": {\n "10000": {\n "full_turn_counts": {\n "131072": 32,\n "4096": 23\n },\n "inverse_frequencies": [\n 1.0,\n 0.7498942093324558,\n 0.5623413251903491,\n 0.4216965034285822,\n 0.31622776601683794,\n 0.23713737056616552,\n 0.1778279410038923,\n 0.1333521432163324,\n 0.1,\n 0.07498942093324558,\n 0.056234132519034905,\n 0.042169650342858224,\n 0.03162277660168379,\n 0.023713737056616554,\n 0.01778279410038923,\n 0.01333521432163324,\n 0.01,\n 0.007498942093324558,\n 0.005623413251903491,\n 0.004216965034285822,\n 0.0031622776601683794,\n 0.0023713737056616554,\n 0.0017782794100389228,\n 0.001333521432163324,\n 0.001,\n 0.0007498942093324559,\n 0.0005623413251903491,\n 0.00042169650342858224,\n 0.00031622776601683794,\n 0.0002371373705661655,\n 0.00017782794100389227,\n 0.0001333521432163324\n ],\n "maximum_wavelength_tokens": 47117.2427801674,\n "minimum_wavelength_tokens": 6.283185307179586,\n "wavelengths": [\n 6.283185307179586,\n 8.378762269377678,\n 11.173259061216543,\n 14.899780425245321,\n 19.869176531592203,\n 26.49597274431474,\n 35.33294752055899,\n 47.117242780167395,\n 62.83185307179586,\n 83.78762269377677,\n 111.73259061216544,\n 148.99780425245322,\n 198.69176531592203,\n 264.95972744314736,\n 353.32947520558986,\n 471.1724278016739,\n 628.3185307179587,\n 837.8762269377677,\n 1117.3259061216543,\n 1489.9780425245322,\n 1986.9176531592202,\n 2649.597274431474,\n 3533.294752055899,\n 4711.724278016739,\n 6283.185307179586,\n 8378.762269377676,\n 11173.259061216542,\n 14899.78042524532,\n 19869.176531592202,\n 26495.972744314742,\n 35332.94752055899,\n 47117.2427801674\n ]\n },\n "150000": {\n "full_turn_counts": {\n "131072": 27,\n "4096": 18\n },\n "inverse_frequencies": [\n 1.0,\n 0.6890443058881632,\n 0.4747820554769007,\n 0.3271458718642365,\n 0.22541800020287084,\n 0.15532298948448497,\n 0.10702442147781144,\n 0.07374456821026082,\n 0.050813274815461475,\n 0.03501259767512414,\n 0.02412523106239743,\n 0.016623353091781194,\n 0.011454226792660225,\n 0.007892469749834168,\n 0.00543826134051781,\n 0.0037472030106155264,\n 0.0025819888974716113,\n 0.0017791047476692703,\n 0.0012258819959601083,\n 0.0008446870090071289,\n 0.0005820267738140659,\n 0.00040104223437104005,\n 0.00027633586801403137,\n 0.00019040765636773135,\n 0.00013119931141769537,\n 9.040213846881087e-05,\n 6.229107875204741e-05,\n 4.292131312172942e-05,\n 2.9574686407770566e-05,\n 2.0378269267702367e-05,\n 1.4041530402766067e-05,\n 9.675236569981486e-06\n ],\n "maximum_wavelength_tokens": 649408.9588128396,\n "minimum_wavelength_tokens": 6.283185307179586,\n "wavelengths": [\n 6.283185307179586,\n 9.118695638999144,\n 13.233830627546281,\n 19.206066307286523,\n 27.873485265262175,\n 40.452384595695705,\n 58.70795861748462,\n 85.20200822472704,\n 123.65243787176136,\n 179.4549883296344,\n 260.4404198628719,\n 377.9734011837886,\n 548.5473110420513,\n 796.0987506238595,\n 1155.3665618028806,\n 1676.7667215733509,\n 2433.467205584167,\n 3531.6556348977883,\n 5125.4405627016395,\n 7438.477495427609,\n 10795.354423311823,\n 15667.141185350692,\n 22737.494601535214,\n 32998.59589177952,\n 47890.3832595279,\n 69502.61811945203,\n 100868.14088081702,\n 146388.46880941882,\n 212451.46002727243,\n 308327.72031027393,\n 447471.54526274785,\n 649408.9588128396\n ]\n },\n "500000": {\n "full_turn_counts": {\n "131072": 25,\n "4096": 16\n },\n "inverse_frequencies": [\n 1.0,\n 0.6636012376960885,\n 0.44036660267178046,\n 0.2922278225730151,\n 0.19392274474868576,\n 0.12868737343265052,\n 0.08539710028576561,\n 0.05666962144529105,\n 0.03760603093086393,\n 0.024955408670558694,\n 0.016560440080994446,\n 0.010989528534539826,\n 0.007292664737217109,\n 0.004839421345719893,\n 0.003211445994752591,\n 0.0021311195369119653,\n 0.001414213562373095,\n 0.0009384738703573802,\n 0.000622772421914596,\n 0.0004132725499855165,\n 0.0002742481756762073,\n 0.00018199142881462546,\n 0.00012076973741146504,\n 8.01429472224798e-05,\n 5.318295896944988e-05,\n 3.529227739646723e-05,\n 2.341999896140934e-05,\n 1.5541540297632344e-05,\n 1.031338537721246e-05,\n 6.8439753011549275e-06,\n 4.5416704806078695e-06,\n 3.013858152139171e-06\n ],\n "maximum_wavelength_tokens": 2084764.7732591254,\n "minimum_wavelength_tokens": 6.283185307179586,\n "wavelengths": [\n 6.283185307179586,\n 9.468314629722128,\n 14.268078616903308,\n 21.500982527458316,\n 32.40045573469106,\n 48.8251888245115,\n 73.57609668424418,\n 110.87395939719457,\n 167.07919319459117,\n 251.77649423117703,\n 379.40931982782695,\n 571.7429357803371,\n 861.5760539647758,\n 1298.3340069648193,\n 1956.497265545218,\n 2948.3026167007256,\n 4442.882938158366,\n 6695.109481084312,\n 10089.055144515105,\n 15203.490547339246,\n 22910.58196353461,\n 34524.62211052572,\n 52026.15689866611,\n 78399.72854675821,\n 118142.83050307268,\n 178032.86641424094,\n 268282.9028956321,\n 404283.3069857812,\n 609226.2702664398,\n 918060.7805699259,\n 1383452.483839961,\n 2084764.7732591254\n ]\n }\n },\n "contexts": [\n 4096,\n 131072\n ],\n "head_dim": 64\n },\n "worked_rope": {\n "base": 10000.0,\n "head_dim": 4,\n "k": [\n 0.0,\n 1.0,\n 1.0,\n -0.5\n ],\n "logits": [\n 1.1442484284723262,\n 1.1442484284723262,\n 1.1442484284723264\n ],\n "placements": [\n [\n 3,\n 1\n ],\n [\n 7,\n 5\n ],\n [\n 2,\n 0\n ]\n ],\n "q": [\n 1.0,\n 0.0,\n 0.5,\n 0.5\n ],\n "rotated_k_at_1": [\n -0.8414709848078965,\n 0.5403023058681398,\n 1.0049499170837486,\n -0.489975166874166\n ],\n "rotated_q_at_3": [\n -0.9899924966004454,\n 0.1411200080598672,\n 0.48477726677324595,\n 0.5147727669757416\n ]\n }\n}\n')
CONFIG_BYTES = b'{"num_hidden_layers": 36, "num_experts": 128, "experts_per_token": 4, "vocab_size": 201088, "hidden_size": 2880, "intermediate_size": 2880, "swiglu_limit": 7.0, "head_dim": 64, "num_attention_heads": 64, "num_key_value_heads": 8, "sliding_window": 128, "initial_context_length": 4096, "rope_theta": 150000, "rope_scaling_factor": 32.0, "rope_ntk_alpha": 1, "rope_ntk_beta": 32}'
assert hashlib.sha256(CONFIG_BYTES).hexdigest() == '063aaa98e012cac330d58d18471700f0b0eb90bb15c58fb227809d45090f3e65'
print("Source:", 'https://huggingface.co/openai/gpt-oss-120b/resolve/b5c939de8f754692c1647ca79fbf85e8c1e70f8a/original/config.json')
config = json.loads(CONFIG_BYTES)
sc = reference['scaling']
assert (config['head_dim'], config['rope_theta'], config['initial_context_length'], config['rope_scaling_factor']) == (sc['head_dim'], sc['base'], sc['original_context'], sc['factor'])
raw = rope_inverse_frequencies(config['head_dim'], config['rope_theta'])
yarn = yarn_scaled_frequencies(config['head_dim'], config['rope_theta'], config['rope_scaling_factor'], config['initial_context_length'])
np.testing.assert_allclose(yarn.inverse_frequencies, sc['yarn']['inverse_frequencies'], rtol=1e-12, atol=1e-15)
np.testing.assert_allclose(ntk_scaled_frequencies(sc['head_dim'],sc['base'],sc['factor']),sc['ntk'],rtol=1e-12,atol=1e-15)
scaling_plot(reference, DEMO_LANGUAGE)
plt.show()
print('YaRN bounds:', yarn.low, yarn.high)
print('Q/K amplitude and full-head score factor:', yarn.attention_factor, yarn.score_factor_if_applied_to_q_and_k)Границы рампы около 8.0928 и 17.3980 делят пары на 9 неизменных, 9 переходных и 14 интерполированных. Масштаб 1.346574 для каждого полностью вращаемого вектора даёт около 1.813260 в скалярном произведении. Это восстановленная геометрия; качество на длинном тексте и история обучения здесь не измерялись.
Демо 2. Проверить, какой оси принадлежит изменение
В секционной M-RoPE каждой оси выделены свои пары координат. Увеличим только координату ширины изображения и проверим, какие компоненты повернутого вектора изменились. Затем подставим одинаковые ID по трём осям для текста и сравним выход с обычной одномерной RoPE. Оба сравнения используют те же частоты и явный численный допуск, поэтому проверяют конкретные алгебраические свойства.
mref = reference['mrope']
x = np.array(mref['image_input'])
coords = np.array(mref['image_coordinates'], dtype=np.int64)
inv = rope_inverse_frequencies(mref['head_dim'], mref['base'])
rotated = apply_mrope_adjacent(x, coords, mref['axis_sections'], inv)
np.testing.assert_allclose(rotated,mref['image_output'],rtol=1e-12,atol=1e-12)
np.testing.assert_allclose(np.linalg.norm(rotated,axis=-1),np.linalg.norm(x,axis=-1),rtol=1e-12)
changed_coords=coords.copy();changed_coords[:,2]+=1
width_changed=apply_mrope_adjacent(x,changed_coords,mref['axis_sections'],inv)
np.testing.assert_array_equal(width_changed[:,:6],rotated[:,:6])
assert not np.allclose(width_changed[:,6:],rotated[:,6:])
text_pos=np.array([4,5,6,7],dtype=np.int64)
text_axes=np.repeat(text_pos[:,None],3,axis=1)
np.testing.assert_allclose(apply_mrope_adjacent(x,text_axes,mref['axis_sections'],inv),apply_rope_adjacent(x,text_pos,inv),rtol=1e-12,atol=1e-12)
print('Image rotations:',rotated)
print('Only width pairs change; equal text IDs recover 1D RoPE: PASS')При секциях (2,1,1) оси ширины принадлежит последняя пара, координаты 6 и 7. Остальные компоненты сохраняются, а нормы всех векторов остаются прежними. Совпадение текстового пути с 1D-RoPE объясняется равными ID осей. Попробуйте изменить высоту вместо ширины и сначала назовите ожидаемую пару.
Переход к полному проекту
Для полного проекта используйте модель и проверочный корпус с зафиксированными ревизиями. Проверьте раскладку пар и позиции кэша, затем отдельно измерьте качество на разных длинах, память и время. Сходство позиционных векторов не является метрикой качества языка.
Минимальный отчёт
-
Версии данных, модели, кода и seed.
-
Основная метрика, а также качество, стоимость, задержка и память там, где это применимо.
-
Базовая линия, одинаковый бюджет и хотя бы один отрицательный контроль.
-
Неудачные прогоны и границы обоснованных выводов.