← Все главы

29 · Версия материала 3

Поворачивайте пары координат запросов и ключей с помощью RoPE

Разберитесь, как ротационное позиционное кодирование поворачивает пары координат запросов и ключей в зависимости от абсолютной позиции, чтобы в скалярных произведениях внимания учитывалось относительное положение, и реализуйте его на Rust.

Сначала предскажите поворот одной пары

Возьмём одну двухкоординатную пару запроса и ключа с первой частотой:

q=[1,0],k=[0,1],θ0=1.q=[1,0],\qquad k=[0,1],\qquad \theta_0=1.

При одинаковых позициях m=n=0m=n=0 ни один вектор не поворачивается, поэтому

qk=0.q^\top k=0.

Теперь поместим запрос в позицию m=1m=1, а ключ оставим в позиции n=0n=0:

R(1)q[0.540302,0.841471],R(0)k=[0,1].R(1)q\approx[0.540302,0.841471],\qquad R(0)k=[0,1].

Их скалярное произведение приблизительно равно 0.8414710.841471. Сдвинем обе абсолютные позиции на 22: для (m,n)=(3,2)(m,n)=(3,2) по-прежнему выполняется nm=1n-m=-1, поэтому при тех же векторах скалярное произведение останется приблизительно равным 0.8414710.841471. Обе повёрнутые пары изменятся, но их относительный поворот — нет.

Исполняемый пример расширяет этот расчёт до d=4d=4. Строка [1,0,1,0][1,0,1,0] повторяется в позициях 00, 11 и 22, а при b=100b=100 получаются частоты

[θ0,θ1]=[1,0.1].[\theta_0,\theta_1]=[1,0.1].

До запуска предскажите, какая пара поворачивается быстрее, что произойдёт в позиции 00 и какие ячейки совпадут после сдвига всех позиций запросов и ключей на 33 при неизменном содержимом.

Поворачивайте соседние координаты по абсолютной позиции

Для каждой соседней пары координат

(RoPE(xm))2k:2k+2=R(mθk)(xm)2k:2k+2\left(\operatorname{RoPE}(x_m)\right)_{2k:2k+2}=R(m\theta_k)(x_m)_{2k:2k+2}

Матрица поворота и расписание частот имеют вид

R(ϕ)=[cosϕsinϕsinϕcosϕ],θk=b2k/d.R(\phi)= \begin{bmatrix} \cos\phi&-\sin\phi\\ \sin\phi&\cos\phi \end{bmatrix}, \qquad \theta_k=b^{-2k/d}.

В этой реализации образуют пары координаты 2k2k и 2k+12k+1. В других реализациях пары могут располагаться иначе. Здесь dd должно быть ненулевым и чётным, чтобы у каждой координаты был партнёр.

В позиции m=0m=0 угол любой пары равен нулю, а каждый поворот ортогонален:

R(0)=I,R(ϕ)R(ϕ)=I,R(ϕ)x2=x2.R(0)=I, \qquad R(\phi)^\top R(\phi)=I, \qquad \lVert R(\phi)x\rVert_2=\lVert x\rVert_2.

Когда запрос в позиции mm сопоставляется с ключом в позиции nn,

(R(mθk)qm(k))(R(nθk)kn(k))=(qm(k))R((nm)θk)kn(k).\bigl(R(m\theta_k)q_m^{(k)}\bigr)^\top \bigl(R(n\theta_k)k_n^{(k)}\bigr) =(q_m^{(k)})^\top R((n-m)\theta_k)k_n^{(k)}.

Это следует из тождества

R(α)R(β)=R(βα).R(\alpha)^\top R(\beta)=R(\beta-\alpha).

На вход поворотов поступают абсолютные позиции. Знаковая разность nmn-m возникает лишь при объединении двух поворотов в скалярном произведении. Оценка по-прежнему зависит от содержимого запроса и ключа и от всех θk\theta_k, а не только от разности позиций.

Пусть LL — скалярная функция потерь, yˉ=L/y\bar{y}=\partial L/\partial y — выходная сопряжённая величина, а xˉ=L/x\bar{x}=\partial L/\partial x — сопряжённая величина входа. В обратном режиме применяется транспонированный поворот:

[xˉ2kxˉ2k+1]=R(mθk)[yˉ2kyˉ2k+1].\begin{bmatrix}\bar{x}_{2k}\\\bar{x}_{2k+1}\end{bmatrix} =R(m\theta_k)^\top \begin{bmatrix}\bar{y}_{2k}\\\bar{y}_{2k+1}\end{bmatrix}.

Разделяйте роли позиции, пары и видимости

  • qm(k)q_m^{(k)} и kn(k)k_n^{(k)} — двухкоординатные пары запроса и ключа в абсолютных позициях mm и nn до поворота.
  • kk выбирает соседнюю пару координат и её частоту θk\theta_k.
  • ϕ\phi — произвольный угол поворота, а II — единичная матрица.
  • nmn-m — знаковая разность позиции ключа и позиции запроса, а не расстояние без знака.
  • Черта обозначает производную LL: yˉ\bar{y} приходит от последующих операций, а xˉ\bar{x} передаётся предыдущим.

До наложения маски самовнимание без позиционного сигнала эквивариантно относительно согласованной перестановки строк содержимого: если вместе переставить запросы, ключи и значения, выходы переставятся так же. RoPE добавляет в оценки запрос–ключ геометрию относительных позиций, но не управляет видимостью. Каузальная маска из главы 28 по-прежнему закрывает будущие ключи, а значения VV здесь не поворачиваются.

От порядка в рекуррентном состоянии к геометрии RoPE

В рекуррентных нейронных языковых моделях порядок переносился через состояние, обновляемое по одному токену; самовнимание без маски и позиционного сигнала вместо этого эквивариантно относительно согласованной перестановки строк содержимого.

Vaswani и соавторы описывают исходный Transformer. Vaswani и соавторы отказываются от рекуррентности и свёрток, добавляют позиционные кодирования к входным эмбеддингам и сравнивают фиксированное синусоидальное представление позиций с обучаемым. Синусоидальный вариант был мотивирован гипотезой о возможности обучить модель учитывать фиксированные смещения, но это не гарантирует перенос на любую более длинную последовательность.

Аддитивный подход изменяет эмбеддинг ещё до получения запроса, ключа или значения. Небольшая функция на Rust показывает эту операцию непосредственно:

Добавьте синусоидальный вектор позиции к одному эмбеддингу rust/demos/ch29-rope/src/lib.rs#historical-position-contrast
/// Add the original Transformer's sinusoidal position vector to one embedding.
/// RoPE instead leaves the embedding unchanged and rotates query/key pairs later.
pub fn add_sinusoidal_position(mut embedding: [f64; 4], position: usize) -> [f64; 4] {
    let position = position as f64;
    let fast_angle = position;
    let slow_angle = position / 100.0;
    embedding[0] += fast_angle.sin();
    embedding[1] += fast_angle.cos();
    embedding[2] += slow_angle.sin();
    embedding[3] += slow_angle.cos();
    embedding
}

Su и соавторы представляют RoFormer. Su и соавторы кодируют абсолютную позицию поворотами подпространств запросов и ключей и выводят скалярное произведение самовнимания, позиционный член которого использует разность их позиций. В отличие от аддитивного кодирования на входе исходного Transformer, повороты запросов и ключей в RoFormer добавляют в скалярное произведение член, зависящий от знаковой разности позиций.

Touvron и соавторы описывают исходную LLaMA. Touvron и соавторы указывают, что в исходной LLaMA абсолютные позиционные эмбеддинги удалены, а RoPE применяется в каждом слое Transformer. В исходной LLaMA RoPE меняет геометрию оценок запрос–ключ в каждом слое Transformer, а каузальная маска отдельно управляет видимостью. Из этого не следует, что RoPE используют все LLM, что оно гарантирует перенос на любую длину или что целый декодер инвариантен к сдвигу входа.

Вычислите таблицы один раз и поворачивайте каждую строку

RotaryEmbedding::new проверяет ненулевую чётную ширину, ненулевую ёмкость по позициям и положительное конечное основание. Кроме того, проверяется, что число ячеек «позиция–пара» помещается в usize, длина каждого массива допустима для Vec<f64>, а каждая обратная частота конечна и неотрицательна. Для одной пары при умножении такой частоты на возрастающие неотрицательные позиции угол не может уменьшиться. Синус и косинус конечного угла также конечны. Поэтому можно доказать конечность всех ячеек таблицы, проверив для каждой пары только последнюю позицию. Если последний угол не конечен, двоичный поиск находит первую позицию с ошибкой, а наименьшая из найденных пар «позиция–пара» сохраняет порядок ошибок при построчном обходе.

Объём этой проверки растёт с числом пар координат, а для пары с ошибкой — ещё и с логарифмом ёмкости по позициям, но не с числом ячеек таблицы. Память под таблицы при этом не выделяется, и проверка не выясняет, сможет ли распределитель памяти удовлетворить допустимый по размеру запрос: об этой ошибке может сообщить только конструктор. Затем конструктор выделяет память под три массива, обходит все ячейки построчно и сохраняет одну обратную частоту для каждой пары и по одной строке синусов и косинусов для каждой абсолютной позиции. Таким образом, декодер может проверить логическую таблицу RoPE, заданную шириной головы и максимальным числом позиций, не создавая временную таблицу. Сами таблицы по-прежнему хранятся только в создавшем их компоненте внимания.

Докажите конечность всех значений таблиц RoPE и создайте сами таблицы только в конструкторе rust/crates/llm-from-scratch/src/attention/rope.rs#rope-tables
    /// Precomputes one sine/cosine row per absolute position.
    pub fn new(feature_width: usize, max_positions: usize, base: f64) -> Result<Self, RopeError> {
        let layout = Self::validate_table_layout(feature_width, max_positions, base)?;
        let mut frequencies = reserved_values(layout.pairs)?;
        Self::visit_inverse_frequencies(feature_width, base, |_, frequency| {
            frequencies.push(frequency);
        })?;

        let mut cosines = reserved_values(layout.table_elements)?;
        let mut sines = reserved_values(layout.table_elements)?;
        Self::visit_table_values(
            max_positions,
            layout.pairs,
            |pair| frequencies[pair],
            |_, _, sine, cosine| {
                cosines.push(cosine);
                sines.push(sine);
            },
        )?;

        Ok(Self {
            feature_width,
            max_positions,
            base,
            inverse_frequencies: Tensor::from_vec(vec![layout.pairs], frequencies)?,
            cosines: Tensor::from_vec(vec![max_positions, layout.pairs], cosines)?,
            sines: Tensor::from_vec(vec![max_positions, layout.pairs], sines)?,
        })
    }

    /// Proves the deterministic table is finite without allocating its storage.
    pub(crate) fn validate_table_specification(
        feature_width: usize,
        max_positions: usize,
        base: f64,
    ) -> Result<(), RopeError> {
        let layout = Self::validate_table_layout(feature_width, max_positions, base)?;
        Self::validate_value_capacity(layout.pairs)?;
        Self::visit_inverse_frequencies(feature_width, base, |_, _| {})?;
        Self::validate_value_capacity(layout.table_elements)?;
        Self::validate_table_values(max_positions, layout.pairs, |pair| {
            Self::inverse_frequency(feature_width, base, pair)
        })
    }

    fn validate_table_layout(
        feature_width: usize,
        max_positions: usize,
        base: f64,
    ) -> Result<RotaryTableLayout, RopeError> {
        if feature_width == 0 {
            return Err(RopeError::ZeroFeatureWidth);
        }
        if !feature_width.is_multiple_of(2) {
            return Err(RopeError::OddFeatureWidth {
                width: feature_width,
            });
        }
        if max_positions == 0 {
            return Err(RopeError::ZeroPositionCapacity);
        }
        if !base.is_finite() || base <= 0.0 {
            return Err(RopeError::InvalidBase { base });
        }

        let pairs = feature_width / 2;
        let table_elements =
            max_positions
                .checked_mul(pairs)
                .ok_or(RopeError::TableSizeOverflow {
                    positions: max_positions,
                    pairs,
                })?;
        Ok(RotaryTableLayout {
            pairs,
            table_elements,
        })
    }

    fn validate_value_capacity(elements: usize) -> Result<(), RopeError> {
        let maximum = (isize::MAX as usize) / std::mem::size_of::<f64>();
        if elements > maximum {
            return Err(RopeError::TableAllocationFailed { elements });
        }
        Ok(())
    }

    fn inverse_frequency(feature_width: usize, base: f64, pair: usize) -> f64 {
        let exponent = -2.0 * (pair as f64) / (feature_width as f64);
        base.powf(exponent)
    }

    fn visit_inverse_frequencies(
        feature_width: usize,
        base: f64,
        mut visit: impl FnMut(usize, f64),
    ) -> Result<(), RopeError> {
        let pairs = feature_width / 2;
        for pair in 0..pairs {
            let frequency = Self::inverse_frequency(feature_width, base, pair);
            if !frequency.is_finite() {
                return Err(RopeError::NonFiniteTableValue { position: 0, pair });
            }
            visit(pair, frequency);
        }
        Ok(())
    }

    fn visit_table_values(
        max_positions: usize,
        pairs: usize,
        mut frequency: impl FnMut(usize) -> f64,
        mut visit: impl FnMut(usize, usize, f64, f64),
    ) -> Result<(), RopeError> {
        for position in 0..max_positions {
            for pair in 0..pairs {
                let (sine, cosine) = Self::table_value(position, frequency(pair))
                    .ok_or(RopeError::NonFiniteTableValue { position, pair })?;
                visit(position, pair, sine, cosine);
            }
        }
        Ok(())
    }

    fn validate_table_values(
        max_positions: usize,
        pairs: usize,
        mut frequency: impl FnMut(usize) -> f64,
    ) -> Result<(), RopeError> {
        let last_position = max_positions - 1;
        let mut first_nonfinite = None;
        for pair in 0..pairs {
            let frequency = frequency(pair);
            debug_assert!(frequency.is_finite());
            if Self::table_value(last_position, frequency).is_some() {
                continue;
            }

            let mut last_finite = 0;
            let mut first_invalid = last_position;
            debug_assert!(Self::table_value(last_finite, frequency).is_some());
            while last_finite + 1 < first_invalid {
                let middle = last_finite + (first_invalid - last_finite) / 2;
                if Self::table_value(middle, frequency).is_some() {
                    last_finite = middle;
                } else {
                    first_invalid = middle;
                }
            }
            let candidate = (first_invalid, pair);
            first_nonfinite = Some(match first_nonfinite {
                Some(current) => std::cmp::min(current, candidate),
                None => candidate,
            });
        }
        match first_nonfinite {
            Some((position, pair)) => Err(RopeError::NonFiniteTableValue { position, pair }),
            None => Ok(()),
        }
    }

    fn table_value(position: usize, frequency: f64) -> Option<(f64, f64)> {
        let angle = (position as f64) * frequency;
        let (sine, cosine) = angle.sin_cos();
        (angle.is_finite() && sine.is_finite() && cosine.is_finite())
            .then(|| (canonical_zero(sine), canonical_zero(cosine)))
    }

RotaryEmbedding::rotate считает предпоследнюю ось осью токенов, а последнюю — осью признаков. Поддерживаются формы [T,d][T,d], [B,T,d][B,T,d], [B,H,T,d][B,H,T,d] и независимые ведущие оси. Для заданного смещения oo проверяется интервал

[o,o+T).[o,o+T).

Пустой интервал токенов может начинаться на границе ёмкости таблицы, а непустой не может выходить за неё.

Проверьте формы и выберите интервал абсолютных позиций rust/crates/llm-from-scratch/src/attention/rope.rs#rope-rotation
    /// Rotates the final feature axis at consecutive absolute positions.
    ///
    /// The penultimate axis is the token axis. Any earlier axes are independent
    /// lanes, so rank-two, batched, and batched-head layouts share one rule.
    pub fn rotate(
        &self,
        input: &TensorValue,
        position_offset: usize,
    ) -> Result<TensorValue, RopeError> {
        let shape = input.shape();
        if shape.len() < 2 {
            return Err(RopeError::InputRank { rank: shape.len() });
        }
        let actual_width = shape[shape.len() - 1];
        if actual_width != self.feature_width {
            return Err(RopeError::FeatureWidthMismatch {
                expected: self.feature_width,
                actual: actual_width,
            });
        }
        let tokens = shape[shape.len() - 2];
        let position_end =
            position_offset
                .checked_add(tokens)
                .ok_or(RopeError::PositionOffsetOverflow {
                    offset: position_offset,
                    tokens,
                })?;
        if position_end > self.max_positions {
            return Err(RopeError::PositionRangeExceeded {
                offset: position_offset,
                tokens,
                max_positions: self.max_positions,
            });
        }

        let pairs = self.feature_width / 2;
        let start = position_offset
            .checked_mul(pairs)
            .ok_or(RopeError::TableSizeOverflow {
                positions: position_offset,
                pairs,
            })?;
        let end = position_end
            .checked_mul(pairs)
            .ok_or(RopeError::TableSizeOverflow {
                positions: position_end,
                pairs,
            })?;
        let table_shape = [tokens, pairs];
        let cosines = copy_table_slice(&self.cosines.as_slice()[start..end], &table_shape)?;
        let sines = copy_table_slice(&self.sines.as_slice()[start..end], &table_shape)?;
        input.rotary_pairs(&cosines, &sines).map_err(Into::into)
    }

Дифференцируемая операция посещает каждую соседнюю пару один раз и не строит плотную матрицу размера d×dd\times d. В обратной ветви для произведения вектора на якобиан применяется транспонированный поворот:

Поверните соседние пары и выполните обратный проход транспонированным поворотом rust/crates/llm-from-scratch/src/autograd/model_ops.rs#rotary-pairs-forward
fn rotary_pairs_forward(
    input: &Tensor,
    cosines: &Tensor,
    sines: &Tensor,
    inverse: bool,
) -> Result<Tensor, TensorAutodiffError> {
    debug_assert!(input.rank() >= 2);
    debug_assert_eq!(cosines.shape(), sines.shape());
    debug_assert_eq!(cosines.rank(), 2);

    let width = input.shape()[input.rank() - 1];
    let tokens = input.shape()[input.rank() - 2];
    let pairs = width / 2;
    debug_assert_eq!(cosines.shape(), [tokens, pairs]);

    let mut output = zeros(input.shape())?;
    if input.is_empty() {
        return Ok(output);
    }

    let rows = input.len() / width;
    for row in 0..rows {
        let token = row % tokens;
        for pair in 0..pairs {
            let feature = row * width + pair * 2;
            let table = token * pairs + pair;
            let left = input.as_slice()[feature];
            let right = input.as_slice()[feature + 1];
            let cosine = cosines.as_slice()[table];
            let sine = sines.as_slice()[table];
            let (rotated_left, rotated_right) = if inverse {
                (left * cosine + right * sine, -left * sine + right * cosine)
            } else {
                (left * cosine - right * sine, left * sine + right * cosine)
            };
            output.as_mut_slice()[feature] = canonical_zero(rotated_left);
            output.as_mut_slice()[feature + 1] = canonical_zero(rotated_right);
        }
    }
    Ok(output)
}

fn canonical_zero(value: f64) -> f64 {
    if value == 0.0 { 0.0 } else { value }
}

Одна и та же операция независимо вызывается для QQ и KK. Центральные разности проверяют все 1212 координат каждого входа при

h=106,εg=4×106.h=10^{-6},\qquad \varepsilon_g=4\times10^{-6}.

Пример также проверяет знаки, нормы, все ячейки матрицы скалярных произведений, одинаковый сдвиг позиций, формы от второго до четвёртого ранга, пустые оси, недопустимые конфигурации, выход за диапазон позиций, конечность сохранённых значений и воспроизводимость результата. Выполните cargo run --quiet --locked -p ch29-rope, чтобы изучить все значения. Исполняемый пример строит и печатает этот детерминированный отчёт:

Запустите полный пример RoPE rust/demos/ch29-rope/src/main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let evidence = ch29_rope::learner_evidence()?;
    print!("{}", ch29_rope::render_report(&evidence));
    Ok(())
}

Сопоставьте повороты пар с диагоналями матрицы

Проследите, как абсолютные повороты выявляют взаимное положение запроса и ключа

Сравните частоты двух пар, их повороты, матрицу скалярных произведений запросов и ключей, проверку одинакового сдвига, величины обратного прохода, допустимые формы, недопустимые граничные случаи и путь к современным декодерным LLM.

  • Быстрая пара: сплошная рамка
  • Медленная пара: штриховая рамка
  • Одинаковые позиции: двойная рамка
  • Ключ расположен позже: сплошная рамка
  • Ключ расположен раньше: штриховая рамка

Проследите две пары координат в трёх абсолютных позициях

Первая пара поворачивается на один радиан за позицию, вторая — на одну десятую радиана.

(RoPE(xm))2k:2k+2=R(mθk)(xm)2k:2k+2\left(\operatorname{RoPE}(x_m)\right)_{2k:2k+2}=R(m\theta_k)(x_m)_{2k:2k+2}
Первая пара поворачивается на один радиан за позицию, вторая — на одну десятую радиана.
Абсолютная позиция Пара координат Координаты признаков Радиан на позицию Абсолютный угол До поворота После поворота
m=0m=0 k=0k=0Быстрая пара: сплошная рамка [0,1][0,1] θ0=1.000000\theta_0=1.000000 0.0000000.000000 [1.000000,0.000000][1.000000,0.000000] [1.000000,0.000000][1.000000,0.000000]
m=0m=0 k=1k=1Медленная пара: штриховая рамка [2,3][2,3] θ1=0.100000\theta_1=0.100000 0.0000000.000000 [1.000000,0.000000][1.000000,0.000000] [1.000000,0.000000][1.000000,0.000000]
m=1m=1 k=0k=0Быстрая пара: сплошная рамка [0,1][0,1] θ0=1.000000\theta_0=1.000000 1.0000001.000000 [1.000000,0.000000][1.000000,0.000000] [0.540302,0.841471][0.540302,0.841471]
m=1m=1 k=1k=1Медленная пара: штриховая рамка [2,3][2,3] θ1=0.100000\theta_1=0.100000 0.1000000.100000 [1.000000,0.000000][1.000000,0.000000] [0.995004,0.099833][0.995004,0.099833]
m=2m=2 k=0k=0Быстрая пара: сплошная рамка [0,1][0,1] θ0=1.000000\theta_0=1.000000 2.0000002.000000 [1.000000,0.000000][1.000000,0.000000] [0.416147,0.909297][-0.416147,0.909297]
m=2m=2 k=1k=1Медленная пара: штриховая рамка [2,3][2,3] θ1=0.100000\theta_1=0.100000 0.2000000.200000 [1.000000,0.000000][1.000000,0.000000] [0.980067,0.198669][0.980067,0.198669]

Найдите знаковые разности позиций в матрице запрос–ключ

Поскольку содержимое во всех позициях повторяется, значения с одинаковой знаковой разностью позиций повторяются по диагоналям.

(R(mθk)q)(R(nθk)k)=qR((nm)θk)k\left(R(m\theta_k)q\right)^\top\left(R(n\theta_k)k\right)=q^\top R((n-m)\theta_k)k
Скалярное произведение после поворота — Положение ключа относительно запроса. Поскольку содержимое во всех позициях повторяется, значения с одинаковой знаковой разностью позиций повторяются по диагоналям.
Позиция запроса / Позиция ключа n=0n=0n=1n=1n=2n=2
m=0m=0 2.0000002.000000 nm=0n-m=0 · Одинаковые позиции: двойная рамка 1.5353061.535306 nm=1n-m=1 · Ключ расположен позже: сплошная рамка 0.5639200.563920 nm=2n-m=2 · Ключ расположен позже: сплошная рамка
m=1m=1 1.5353061.535306 nm=1n-m=-1 · Ключ расположен раньше: штриховая рамка 2.0000002.000000 nm=0n-m=0 · Одинаковые позиции: двойная рамка 1.5353061.535306 nm=1n-m=1 · Ключ расположен позже: сплошная рамка
m=2m=2 0.5639200.563920 nm=2n-m=-2 · Ключ расположен раньше: штриховая рамка 1.5353061.535306 nm=1n-m=-1 · Ключ расположен раньше: штриховая рамка 2.0000002.000000 nm=0n-m=0 · Одинаковые позиции: двойная рамка
Матрица скалярных произведений для позиций от нуля до двух

Исходные абсолютные позиции: [0,1,2][0,1,2]

Матрица скалярных произведений для позиций от нуля до двух
Позиция запроса / Позиция ключаn=0n=0n=1n=1n=2n=2
m=0m=02.0000002.0000001.5353061.5353060.5639200.563920
m=1m=11.5353061.5353062.0000002.0000001.5353061.535306
m=2m=20.5639200.5639201.5353061.5353062.0000002.000000
Матрица скалярных произведений для позиций от трёх до пяти

Позиции после одинакового сдвига: [3,4,5][3,4,5]

Матрица скалярных произведений для позиций от трёх до пяти
Позиция запроса / Позиция ключаn=3n=3n=4n=4n=5n=5
m=3m=32.0000002.0000001.5353061.5353060.5639200.563920
m=4m=41.5353061.5353062.0000002.0000001.5353061.535306
m=5m=50.5639200.5639201.5353061.5353062.0000002.000000
Проверка одинакового сдвига при фиксированном содержимом

Проверено

ε=0.000000000001\varepsilon=0.000000000001

Проверьте нормы, формы, границы и обратный проход

Ортогональные повороты сохраняют нормы и формы, а транспонированная матрица переносит выходные сопряжённые величины ко входам.

Сохранение нормы
До поворота
[1.414214,1.414214,1.414214][1.414214,1.414214,1.414214]
После поворота
[1.414214,1.414214,1.414214][1.414214,1.414214,1.414214]
Позиции после одинакового сдвига
[1.414214,1.414214,1.414214][1.414214,1.414214,1.414214]

Проверено

Сохранение форм тензоров
  • Оси пакета, токенов и признаков [2,3,4][2,3,4][2,3,4]\to[2,3,4] Проверено
  • Оси пакета, голов, токенов и признаков [2,2,3,4][2,2,3,4][2,2,3,4]\to[2,2,3,4] Проверено
  • Пустая ведущая ось [0,3,4][0,3,4][0,3,4]\to[0,3,4] Проверено
  • Пустая ось токенов у границы таблицы [2,0,4][2,0,4][2,0,4]\to[2,0,4] Проверено
Недопустимые граничные случаи
  • Нечётная ширина последней оси признаков odd-feature-width Проверено
  • Ранг входа меньше двух input-rank Проверено
  • Настроенная ширина не совпадает с шириной входа feature-width-mismatch Проверено
  • Интервал позиций выходит за таблицу position-range-exceeded Проверено
  • Переполнение интервала позиций position-offset-overflow Проверено
  • Освобождённый операнд автоматического дифференцирования autodiff-stage Проверено
Транспонированный поворот в обратном режиме
Входной тензорВыходная сопряжённая величинаСопряжённая величина входа
QQ[1.0000000.5000000.2500000.7500000.3000000.8000001.2000000.4000000.6000000.1000000.7000000.900000]\begin{bmatrix}1.000000&-0.500000&0.250000&0.750000\\-0.300000&0.800000&1.200000&-0.400000\\0.600000&0.100000&-0.700000&0.900000\end{bmatrix}[1.0000000.5000000.2500000.7500000.5110860.6846831.1540720.5178020.1587580.5871930.5072441.021128]\begin{bmatrix}1.000000&-0.500000&0.250000&0.750000\\0.511086&0.684683&1.154072&-0.517802\\-0.158758&-0.587193&-0.507244&1.021128\end{bmatrix}
KK[0.2000000.4000000.9000000.6000000.5000001.1000000.8000000.3000001.0000000.9000000.2000000.700000]\begin{bmatrix}-0.200000&0.400000&0.900000&-0.600000\\0.500000&1.100000&-0.800000&0.300000\\1.000000&-0.900000&0.200000&0.700000\end{bmatrix}[0.2000000.4000000.9000000.6000001.1957690.1735970.7660530.3783681.2345150.5347650.3350820.646313]\begin{bmatrix}-0.200000&0.400000&0.900000&-0.600000\\1.195769&0.173597&-0.766053&0.378368\\-1.234515&-0.534765&0.335082&0.646313\end{bmatrix}
Проверка градиента по всем координатам: 12+1212+12 · εg=0.000004\varepsilon_g=0.000004 · Проверено

Проследите развитие позиционной информации в декодерных LLM

Позиционная информация перешла от последовательного состояния к явным векторам, а затем — в геометрию оценок запрос–ключ.

  1. Порядок в рекуррентном состоянии

    Рекуррентное состояние обновлялось для каждого токена и переносило порядок через последовательные вычисления.

  2. Векторы позиций в эмбеддингах

    В исходном Transformer к эмбеддингам токенов добавлялись явные векторы позиций.

  3. Повороты пар координат запросов и ключей

    RoPE поворачивает пары координат запросов и ключей по абсолютной позиции, поэтому их скалярное произведение зависит от знаковой разности позиций.

  4. RoPE во влиятельной декодерной LLM

    В исходной LLaMA RoPE применяется в каждом слое Transformer.

Видимость задаётся отдельно

RoPE меняет геометрию оценок. Каузальная маска по-прежнему решает, какие ключи доступны запросу.

До одинакового сдвига получается матрица

[21.5353060.5639201.53530621.5353060.5639201.5353062].\begin{bmatrix} 2&1.535306&0.563920\\ 1.535306&2&1.535306\\ 0.563920&1.535306&2 \end{bmatrix}.

В этом примере содержимое запросов и ключей повторяется во всех позициях, поэтому одинаковые знаковые разности позиций дают одинаковые значения на диагоналях. Перенос неизменного содержимого из абсолютных позиций 0,1,20,1,2 в 3,4,53,4,5 меняет повёрнутые координаты, но сохраняет все девять скалярных произведений с точностью 101210^{-12}. Численное сравнение проверяет один пример; общее свойство следует из приведённого выше тождества матриц поворота.

Сначала предскажите, затем проверьте свойство

  1. Докажите, что позиция m=0m=0 задаёт единичный поворот для каждой пары.
  2. Вычислите результат для одной пары при (m,n)=(1,0)(m,n)=(1,0).
  3. Предскажите изменения после одинакового сдвига обеих позиций при фиксированном содержимом запроса и ключа.
  4. Объясните, почему сдвиг только mm обычно меняет скалярное произведение.
  5. Выведите сохранение нормы из R(ϕ)R(ϕ)=IR(\phi)^\top R(\phi)=I.
  6. Решите, нужно ли поворачивать VV или каузальную маску.
  7. Объясните, почему эта реализация с полным попарным поворотом отклоняет d=3d=3.
  8. Разграничьте абсолютные входные позиции и знаковую разность в скалярном произведении.
  9. Объясните, почему локальное свойство при фиксированном содержимом не доказывает инвариантность целого декодера к сдвигу.
Проверьте ключевые различия
  1. При m=0m=0 угол каждой пары равен 00, поэтому R(0)=IR(0)=I.
  2. При (m,n)=(1,0)(m,n)=(1,0) скалярное произведение приблизительно равно 0.8414710.841471.
  3. При добавлении cc к обеим позициям сохраняется (n+c)(m+c)=nm(n+c)-(m+c)=n-m, поэтому для неизменного содержимого скалярное произведение не меняется, хотя оба абсолютных поворота становятся другими.
  4. Сдвиг только mm меняет nmn-m и потому обычно меняет результат; особое содержимое или периодичность углов всё же могут дать прежнее значение.
  5. Тождество R(ϕ)R(ϕ)=IR(\phi)^\top R(\phi)=I сохраняет квадрат нормы, а значит и саму норму.
  6. Здесь поворачиваются QQ и KK, но не VV; каузальная маска независимо определяет видимые ключи.
  7. При ширине 33 у одной координаты не остаётся пары.
  8. Абсолютные индексы задают локальные углы, а знаковая разность появляется лишь после объединения поворотов запроса и ключа.
  9. Для утверждения о целой модели пришлось бы проверить каждый компонент и каждую границу, сохранив те же содержимое и контекст. Одного локального тождества недостаточно.

Заблуждение: RoPE получает на вход относительные индексы.

Исправление: на вход поступают абсолютные позиции. Благодаря алгебре поворотов скалярное произведение фиксированных запроса и ключа зависит от знаковой разности их позиций. RoPE не заменяет каузальную маску, а оценка по-прежнему зависит от содержимого и частоты.

Передайте ось с позиционной информацией нескольким головам

В текущем одноголовом пути каждая строка запроса и ключа поворачивается перед вычислением каузальных оценок из главы 28. В главе 30 проекции запросов и ключей сначала преобразуются в отдельные головы, затем RoPE применяется вдоль последней оси признаков каждой головы, каузальное внимание вычисляется независимо по головам, их выходы объединяются и к результату применяется обучаемая выходная проекция WOW_O.

Частичный поворот признаков, другие схемы образования пар, кэши ключей и значений, предварительная обработка контекста и масштабирование частот для длинного контекста остаются за пределами главы.