← Все главы

11 · Версия материала 5

Умножьте строки на столбцы и используйте одну матрицу весов в нескольких пакетах

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

Предскажите произведение одной строки и одного столбца

Считайте две строки двумя позициями токенов с тремя признаками. Матрица активаций AA имеет форму [2,3]:

[[1, 2, 3],
 [4, 5, 6]]

Матрица весов проекции WW имеет форму [3,2]:

[[1, 2],
 [0, 1],
 [2, 0]]

Предскажите форму результата до умножения значений. Обе внутренние оси имеют размер 33. Два внешних размера сохраняются, поэтому результат имеет форму [2,2].

Теперь предскажите ячейку C1,0C_{1,0}. Выберите строку 11 из AA и столбец 00 из WW, попарно умножьте элементы с одинаковой внутренней координатой и складывайте, начиная с k=0k=0:

C1,0=41+50+62=4+0+12=16.\begin{aligned} C_{1,0} &= 4\cdot1 + 5\cdot0 + 6\cdot2 \\ &= 4 + 0 + 12 \\ &= 16. \end{aligned}

Примените то же правило ко всем четырём парам «строка — столбец»:

Результат CC имеет форму [2,2] и значения:

[[ 7,  4],
 [16, 13]]

Для последующего пакетного примера левый пакет 0 содержит исходную матрицу AA, а левый пакет 1 — следующую матрицу:

[[0, 1, 2],
 [2, 1, 0]]

Правый операнд имеет форму [1,3,2] и хранит исходную матрицу WW ровно один раз. Поскольку размер его единственной начальной оси равен 11, оба пакета результата используют правый пакет 00.

Это не поэлементное умножение: формы AA и WW даже не совпадают. Одна ячейка результата использует целую строку и целый столбец, а общая внутренняя ось исчезает.

Просуммируйте произведения по одной общей внутренней оси

Матричное умножение задаётся формулой:

Cij=k=0K1AikBkjC_{ij}=\sum_{k=0}^{K-1} A_{ik}B_{kj}

В рассматриваемом примере роль общего правого операнда BB играет матрица весов проекции WW, то есть B=WB=W.

Зафиксируйте строку результата ii и столбец jj. Инициализируйте накопленную сумму нулём. Для каждого kk от нуля до K1K-1 умножьте AikA_{ik} на BkjB_{kj} и добавьте произведение к сумме. Порядок цикла — часть этой эталонной реализации, поскольку сложение чисел с плавающей точкой в общем случае неассоциативно.

Для пакетных форм [,M,K][\ldots,M,K] и [,K,N][\ldots,K,N] формула выполняется независимо для каждой совместимой координаты начальных осей пакета. В обозначениях пакетные координаты опущены, чтобы вновь введённое суммирование произведений строки и столбца оставалось на виду. Согласовывать формы можно только по начальным осям пакета; два размера KK должны быть равны.

Назовите каждый индекс и размер матриц

СимволСмысл в вычислении
CijC_{ij}Значение результата в строке ii и столбце jj.
AikA_{ik}Значение левого входа в строке результата ii и позиции kk на общей внутренней оси.
BkjB_{kj}Значение правого входа в позиции kk на общей внутренней оси и столбце результата jj.
iiОтсчитываемый от нуля индекс строки результата.
jjОтсчитываемый от нуля индекс столбца результата.
kkОтсчитываемая от нуля координата, которая проходит по обеим общим внутренним осям.
KKОбщий внутренний размер и, следовательно, число скалярных умножений, произведения которых складываются в одну ячейку.

В результате сохраняются MM строк эффективной левой матрицы и NN столбцов эффективной правой матрицы. Флаг транспонирования логически меняет местами только две последние оси операнда. Поэтому хранимая форма W𝖳W^{\mathsf T} [2,3] может использоваться как логическая форма [3,2], если transpose_right=true; копировать значения не требуется.

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

От одного вектора фиксированного контекста к матрицам всех позиций

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

Эта более ранняя модель описана в статье Bengio et al., A Neural Probabilistic Language Model. Бенжио и соавторы хранят обучаемые признаки слов в матрице, объединяют векторы слов из контекста фиксированной длины и вычисляют оценки следующего слова последовательными обучаемыми матрично-векторными преобразованиями и нелинейным скрытым слоем. В статье это вычисление записано как y=b+Wx+Utanh(d+Hx)y=b+Wx+U\tanh(d+Hx), а обучаемые признаки слов хранятся в матрице CC размера V×m|V|\times m.

Эта модель уже относится к истории LLM: отдельные строки таблиц частот заменены обучаемыми признаками слов и общими параметрами нейронной сети. Существенное ограничение более раннего подхода здесь состоит в том, что он строится вокруг одного объединённого вектора контекста фиксированной длины. В статье не задаются API пакетных тензоров, правила шагов или типы ошибок, используемые в этом курсе.

Более поздние источники — статьи Vaswani et al., Attention Is All You Need и Radford et al., Language Models are Unsupervised Multitask Learners. Васвани и соавторы собирают запросы, ключи и значения в матрицы. Для вычисления внимания они получают масштабированные оценки «запрос — ключ», применяют к ним softmax и используют полученные веса для взвешивания значений; кроме того, они используют обучаемые проекции запросов, ключей и значений, выходную проекцию, а также два линейных преобразования в сети прямого распространения, одинаковой для каждой позиции. В отчёте о GPT-2 описана архитектура авторегрессионной языковой модели на основе Transformer: четыре варианта имеют от 12 до 48 слоёв, ширину от 768 до 1600 и контекст длиной 1024 токена. В обозначениях Transformer преобразование внимания записывается как softmax(QK𝖳/dk)V\operatorname{softmax}(QK^{\mathsf T}/\sqrt{d_k})V.

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

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

Сопоставьте одну взвешенную сумму для фиксированного контекста с суммированием по общей оси, которое позднее повторяется для строк токенов rust/demos/ch11-matrix-multiplication/src/lib.rs#fixed-width-projection
/// Projects one fixed-width context representation with scalar operations.
///
/// This shape-specific baseline makes the older matrix-vector calculation
/// explicit; it is not presented as the exact equation of any cited model.
pub fn fixed_context_projection(context: [f64; 3], weights: [[f64; 2]; 3]) -> [f64; 2] {
    let mut output = [0.0; 2];
    for column in 0..2 {
        for inner in 0..3 {
            output[column] += context[inner] * weights[inner][column];
        }
    }
    output
}

Проверьте формы до начала скалярных циклов

Тип ошибки позволяет различать каждое нарушенное условие. API проверяет ранг левого и правого операндов, эффективные внутренние размеры после применения флагов транспонирования, оси пакета слева направо, полное размещение результата и лишь затем — выделение памяти для результата. Поэтому для запроса, нарушающего несколько правил, первой всегда будет одна и та же ошибка.

Представьте каждую причину отклонения матричного умножения отдельным вариантом ошибки rust/crates/llm-from-scratch/src/tensor/matmul.rs#matmul-errors
/// A rejected matrix product, output layout, allocation, or converted view operation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MatmulError {
    /// An owned output layout violates the tensor storage invariant.
    Tensor(TensorError),
    /// A tensor-view error was converted into the matrix-multiplication error type.
    View(TensorViewError),
    /// Matrix multiplication does not promote a left vector in this chapter.
    LeftRankTooSmall { rank: usize },
    /// Matrix multiplication does not promote a right vector in this chapter.
    RightRankTooSmall { rank: usize },
    /// The effective final left axis and penultimate right axis differ.
    InnerDimensionMismatch { left: usize, right: usize },
    /// Two trailing-aligned batch dimensions are neither equal nor singleton.
    IncompatibleBatch {
        axis: usize,
        left_dimension: usize,
        right_dimension: usize,
    },
    /// The checked output shape is valid, but its value buffer cannot be reserved.
    OutputAllocationFailed { elements: usize },
}

impl fmt::Display for MatmulError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Tensor(error) => error.fmt(formatter),
            Self::View(error) => error.fmt(formatter),
            Self::LeftRankTooSmall { rank } => {
                write!(
                    formatter,
                    "left matmul input must have rank at least 2, got {rank}"
                )
            }
            Self::RightRankTooSmall { rank } => {
                write!(
                    formatter,
                    "right matmul input must have rank at least 2, got {rank}"
                )
            }
            Self::InnerDimensionMismatch { left, right } => write!(
                formatter,
                "matmul inner dimensions do not match: left size {left}, right size {right}"
            ),
            Self::IncompatibleBatch {
                axis,
                left_dimension,
                right_dimension,
            } => write!(
                formatter,
                "cannot broadcast batch axis {axis}: left size {left_dimension}, right size {right_dimension}"
            ),
            Self::OutputAllocationFailed { elements } => write!(
                formatter,
                "cannot allocate output buffer for {elements} f64 values"
            ),
        }
    }
}

impl Error for MatmulError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Tensor(error) => Some(error),
            Self::View(error) => Some(error),
            _ => None,
        }
    }
}

impl From<TensorError> for MatmulError {
    fn from(error: TensorError) -> Self {
        Self::Tensor(error)
    }
}

impl From<TensorViewError> for MatmulError {
    fn from(error: TensorViewError) -> Self {
        Self::View(error)
    }
}

Планировщик сопоставляет, начиная с последних, только те оси, которые предшествуют двум матричным осям. Отсутствующие начальные оси считаются равными единице, а для входной оси единичного размера всегда выбирается координата 00. Планировщик проверяет полную форму результата, а не только её начальные оси пакета. Благодаря этому допустим пустой результат, например для пары входных форм [usize::MAX,2,0,3] и [1,2,3,1]: нулевой размер оси строк делает весь результат пустым ещё до чтения входных данных.

TensorView::get остаётся общедоступным методом для координаты, переданной вызывающим кодом. Такая координата может иметь неверный ранг или выходить за границы оси, поэтому get проверяет каждый вызов. Матричное умножение работает в других условиях: оба представления уже прошли проверку, а планировщик заранее проверил эффективные размеры матриц, совместимость осей пакета и полную форму результата. Поэтому скалярный цикл может повторно использовать один проверенный план смещений, а не строить и проверять заново две координаты для каждого произведения.

Для каждой ячейки результата каждому операнду требуется начальное смещение в хранилище, соответствующее k=0k=0. Назовём его базовым смещением ячейки. Шаг по общей внутренней оси переводит это смещение к значению для k=1k=1, затем для k=2k=2 и так далее.

Для формы результата [...,M,N] план задаёт каждому операнду по одному эффективному шагу для каждой оси результата. Ось пакета, размер которой во входе больше 1, сохраняет шаг входного представления. Отсутствующая ось или ось размера 1 получает шаг 0, чтобы при изменении координаты результата по этой оси выбирался тот же входной пакет. У левого операнда ось строк сохраняет свой эффективный шаг, а шаг по оси столбцов результата равен 0. У правого операнда шаг по оси строк результата равен 0, а ось столбцов сохраняет эффективный шаг по столбцам. Отдельно планировщик сохраняет шаги обоих операндов по общей внутренней оси.

В учебном примере непрерывная матрица AA имеет шаги [3,1], а непрерывная матрица WW — шаги [2,1]. При построчном обходе координат результата [0,0], [0,1], [1,0], [1,1] план [3,0] для левого операнда выдаёт базовые смещения [0,0,3,3], а план [0,1] для правого — [0,1,0,1]. Поэтому для ячейки C1,0C_{1,0} начальные смещения равны 3 и 0. Шаги 1 и 2 по общей внутренней оси дают три пары смещений (3,0), (4,2) и (5,4), по которым в порядке возрастания kk читаются произведения 414\cdot1, 505\cdot0 и 626\cdot2.

Для пакетных форм [2,2,3] и [1,3,2] планы базовых смещений ячеек равны [6,3,0] слева и [0,0,1] справа. Первый 0 в правом плане соответствует единичной оси пакета: пакеты результата 0 и 1 оба начинают чтение с правого пакета 0. Флаг транспонирования меняет местами роли двух последних размеров и шагов операнда, но не перемещает хранимые значения.

Два курсора базовых смещений ячеек проверяются один раз, а затем обходят сначала пакеты, внутри каждого пакета — строки, а внутри строки — столбцы. Внутри каждой ячейки цикл сначала читает левое значение, затем правое, перемножает их и прибавляет произведение к накопленной сумме, последовательно увеличивая kk. Значения по-прежнему читаются обычным безопасным индексированием среза в Rust с проверкой границ. Внутри суммирования не создаётся вектор координат. Для среза или транспонированного представления не нужна неявная материализация: такое представление просто передаёт планировщику другие базовые смещения и шаги.

Один раз спланируйте проверенные базовые смещения ячеек и шаги по общей внутренней оси, а затем накапливайте сумму по смещениям в хранилище rust/crates/llm-from-scratch/src/tensor/matmul.rs#checked-matmul
#[derive(Debug)]
struct MatmulPlan {
    /// The owned result's logical row-major shape and element count.
    output_shape: Vec<usize>,
    output_len: usize,
    /// The number of products accumulated into each output cell.
    inner: usize,
    /// One source-storage movement per output batch, row, and column axis.
    left_cell_strides: Vec<usize>,
    right_cell_strides: Vec<usize>,
    /// Source-storage movement when the contracted index increases by one.
    left_inner_stride: usize,
    right_inner_stride: usize,
}

#[derive(Clone, Copy, Debug)]
struct EffectiveMatrixLayout {
    rows: usize,
    columns: usize,
    row_stride: usize,
    column_stride: usize,
}

/// Multiplies two rank-two or batched tensor views as stored.
pub fn matmul(left: &TensorView<'_>, right: &TensorView<'_>) -> Result<Tensor, MatmulError> {
    matmul_with_transpose(left, right, false, false)
}

/// Multiplies two tensor views after optional logical final-axis transposes.
///
/// Inputs must have rank at least two. Only axes before the final two matrix
/// axes broadcast, using trailing alignment. The effective inner dimensions
/// must match exactly. The scalar contraction visits `k` in ascending order and
/// reads through offsets established by one checked strided plan per operand.
pub fn matmul_with_transpose(
    left: &TensorView<'_>,
    right: &TensorView<'_>,
    transpose_left: bool,
    transpose_right: bool,
) -> Result<Tensor, MatmulError> {
    let plan = MatmulPlan::new(left, right, transpose_left, transpose_right)?;
    let mut values = output_buffer(plan.output_len)?;
    if plan.inner == 0 {
        values.resize(plan.output_len, 0.0);
        return Tensor::from_vec(plan.output_shape, values).map_err(Into::into);
    }

    let left_cell_offsets = left
        .projected_offsets(&plan.output_shape, &plan.left_cell_strides, plan.output_len)
        .expect("a checked matmul plan retains valid left cell offsets");
    let right_cell_offsets = right
        .projected_offsets(
            &plan.output_shape,
            &plan.right_cell_strides,
            plan.output_len,
        )
        .expect("a checked matmul plan retains valid right cell offsets");

    for (left_cell_offset, right_cell_offset) in left_cell_offsets.zip(right_cell_offsets) {
        let mut sum = 0.0;
        let mut left_offset = left_cell_offset;
        let mut right_offset = right_cell_offset;
        for inner_index in 0..plan.inner {
            let left_value = left.value_at_storage_offset(left_offset);
            let right_value = right.value_at_storage_offset(right_offset);
            sum += left_value * right_value;

            if inner_index + 1 < plan.inner {
                left_offset = left_offset
                    .checked_add(plan.left_inner_stride)
                    .expect("a checked matmul plan cannot overflow along the left inner axis");
                right_offset = right_offset
                    .checked_add(plan.right_inner_stride)
                    .expect("a checked matmul plan cannot overflow along the right inner axis");
            }
        }
        values.push(sum);
    }

    Tensor::from_vec(plan.output_shape, values).map_err(Into::into)
}

impl MatmulPlan {
    fn new(
        left: &TensorView<'_>,
        right: &TensorView<'_>,
        transpose_left: bool,
        transpose_right: bool,
    ) -> Result<Self, MatmulError> {
        if left.rank() < 2 {
            return Err(MatmulError::LeftRankTooSmall { rank: left.rank() });
        }
        if right.rank() < 2 {
            return Err(MatmulError::RightRankTooSmall { rank: right.rank() });
        }

        let left_matrix = effective_matrix_layout(left, transpose_left);
        let right_matrix = effective_matrix_layout(right, transpose_right);
        let rows = left_matrix.rows;
        let inner = left_matrix.columns;
        let right_inner = right_matrix.rows;
        let columns = right_matrix.columns;
        if inner != right_inner {
            return Err(MatmulError::InnerDimensionMismatch {
                left: inner,
                right: right_inner,
            });
        }

        let left_batch_shape = &left.shape()[..left.rank() - 2];
        let right_batch_shape = &right.shape()[..right.rank() - 2];
        let batch_shape = broadcast_batch_shape(left_batch_shape, right_batch_shape)?;
        let mut output_shape = batch_shape.clone();
        output_shape.extend([rows, columns]);
        let (_, output_len) = checked_row_major_layout(&output_shape)?;

        let batch_rank = batch_shape.len();
        let mut left_cell_strides = batch_effective_strides(left, batch_rank);
        left_cell_strides.extend([left_matrix.row_stride, 0]);
        let mut right_cell_strides = batch_effective_strides(right, batch_rank);
        right_cell_strides.extend([0, right_matrix.column_stride]);

        Ok(Self {
            output_shape,
            output_len,
            inner,
            left_cell_strides,
            right_cell_strides,
            left_inner_stride: left_matrix.column_stride,
            right_inner_stride: right_matrix.row_stride,
        })
    }
}

fn effective_matrix_layout(input: &TensorView<'_>, transposed: bool) -> EffectiveMatrixLayout {
    let matrix_axis = input.rank() - 2;
    let stored = EffectiveMatrixLayout {
        rows: input.shape()[matrix_axis],
        columns: input.shape()[matrix_axis + 1],
        row_stride: input.strides()[matrix_axis],
        column_stride: input.strides()[matrix_axis + 1],
    };
    if transposed {
        EffectiveMatrixLayout {
            rows: stored.columns,
            columns: stored.rows,
            row_stride: stored.column_stride,
            column_stride: stored.row_stride,
        }
    } else {
        stored
    }
}

fn broadcast_batch_shape(left: &[usize], right: &[usize]) -> Result<Vec<usize>, MatmulError> {
    let output_rank = left.len().max(right.len());
    let left_padding = output_rank - left.len();
    let right_padding = output_rank - right.len();
    let mut output = Vec::with_capacity(output_rank);

    for axis in 0..output_rank {
        let left_dimension = left
            .get(axis.wrapping_sub(left_padding))
            .copied()
            .unwrap_or(1);
        let right_dimension = right
            .get(axis.wrapping_sub(right_padding))
            .copied()
            .unwrap_or(1);
        let dimension = if left_dimension == right_dimension {
            left_dimension
        } else if left_dimension == 1 {
            right_dimension
        } else if right_dimension == 1 {
            left_dimension
        } else {
            return Err(MatmulError::IncompatibleBatch {
                axis,
                left_dimension,
                right_dimension,
            });
        };
        output.push(dimension);
    }
    Ok(output)
}

fn batch_effective_strides(input: &TensorView<'_>, output_batch_rank: usize) -> Vec<usize> {
    let input_batch_rank = input.rank() - 2;
    let padding = output_batch_rank - input_batch_rank;
    (0..output_batch_rank)
        .map(|output_axis| {
            if output_axis < padding {
                return 0;
            }

            let input_axis = output_axis - padding;
            if input.shape()[input_axis] == 1 {
                0
            } else {
                input.strides()[input_axis]
            }
        })
        .collect()
}

Если размер оси пакета, строки или столбца равен нулю, результат пуст и входные данные не читаются. Если равен нулю только KK, ячейки результата всё же существуют, но ни в одном входе нет доступного значения внутренней оси. После выделения памяти для результата реализация записывает в каждую ячейку положительный 0.0, не создавая курсор смещений и не читая ни один вход. Внутренние размеры 00 и 11 не согласуются между собой и отклоняются как несовпадающие.

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

Постройте двумерное произведение, логическое транспонирование правого операнда и пакетное произведение с общей матрицей весов rust/demos/ch11-matrix-multiplication/src/lib.rs#tiny-matmul-example
/// Multiplies the same values as 2-D matrices, a logical transpose, and batches.
pub fn tiny_matrix_multiplication_example() -> Result<TinyMatrixMultiplicationExample, MatmulError>
{
    let token_rows = Tensor::from_vec(TOKEN_SHAPE.to_vec(), TOKEN_VALUES.to_vec())?;
    let weights = Tensor::from_vec(WEIGHT_SHAPE.to_vec(), WEIGHT_VALUES.to_vec())?;
    let product = matmul(&token_rows.view(), &weights.view())?;

    let stored_transpose = Tensor::from_vec(
        STORED_TRANSPOSE_SHAPE.to_vec(),
        STORED_TRANSPOSE_VALUES.to_vec(),
    )?;
    let transpose_product =
        matmul_with_transpose(&token_rows.view(), &stored_transpose.view(), false, true)?;

    let batched_token_rows =
        Tensor::from_vec(BATCHED_TOKEN_SHAPE.to_vec(), BATCHED_TOKEN_VALUES.to_vec())?;
    let batched_weights = Tensor::from_vec(BATCHED_WEIGHT_SHAPE.to_vec(), WEIGHT_VALUES.to_vec())?;
    let batched_product = matmul(&batched_token_rows.view(), &batched_weights.view())?;

    Ok(TinyMatrixMultiplicationExample {
        token_rows,
        weights,
        product,
        stored_transpose,
        transpose_product,
        batched_token_rows,
        batched_weights,
        batched_product,
    })
}

Учебный пример также показывает суммирование при нулевом внутреннем размере и точные ошибки ранга, внутреннего размера и размера пакета.

Получите однозначные результаты двумерного умножения, транспонирования, пакетной обработки, нулевого внутреннего размера и ошибок rust/demos/ch11-matrix-multiplication/src/main.rs#learner-matrix-multiplication-output
    let example = tiny_matrix_multiplication_example()?;

    let zero_inner_left = Tensor::from_vec(vec![2, 0], vec![])?;
    let zero_inner_right = Tensor::from_vec(vec![0, 2], vec![])?;
    let zero_inner = matmul(&zero_inner_left.view(), &zero_inner_right.view())?;

    let wrong_inner = Tensor::from_vec(vec![4, 2], vec![0.0; 8])?;
    let inner_error = matmul(&example.token_rows.view(), &wrong_inner.view()).unwrap_err();

    let wrong_batch = Tensor::from_vec(vec![3, 3, 2], vec![0.0; 18])?;
    let batch_error = matmul(&example.batched_token_rows.view(), &wrong_batch.view()).unwrap_err();

    let rank_one = Tensor::from_vec(vec![3], vec![1.0, 2.0, 3.0])?;
    let rank_error =
        matmul_with_transpose(&rank_one.view(), &example.weights.view(), false, false).unwrap_err();

Результаты примеров с целыми значениями можно сравнивать точно. Для скалярного произведения с десятичными дробями вместо этого используется абсолютный допуск 1e-12, потому что десятичные дроби в общем случае нельзя точно представить двоичными числами с плавающей точкой. Пример со взаимным погашением также делает наблюдаемым порядок накопления по возрастанию kk. В этой главе матричное умножение не делегируется библиотеке.

Проследите одну ячейку результата, затем добавьте ось пакета

Изучите схему в четыре прохода:

  1. Стрелка вправо и сплошная граница выделяют выбранную строку матрицы AA; стрелка вниз и пунктирная граница — выбранный столбец матрицы WW. На их пересечении находится ячейка C1,0C_{1,0} с двойной рамкой.
  2. Просмотрите три карточки суммирования с двойной рамкой в порядке kk. Проверьте произведения 4.0, 0.0 и 12.0, затем накопленные суммы 4.0, 4.0, 16.0.
  3. Сопоставьте хранимую форму правого операнда [2,3] с логической формой [3,2], затем обратите внимание: оба пакета результата используют правый пакет 00.
  4. Рассмотрите пунктирные карточки ошибок. Внутренние размеры 3 и 4 приводят к ошибке до начала скалярного цикла; при совпадающих матричных осях пакетные размеры 2 и 3 приводят к ошибке пакета.

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

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

Форма левой матрицы активаций
[2, 3]
Форма матрицы весов проекции
[3, 2]
Форма произведения
[2, 2]

Выберите одну строку левой матрицы и один столбец правой

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

AA — Форма [2, 3]
Строка Столбец 0Столбец 1Столбец 2
0 1.02.03.0
Выбранная строка левой матрицы: 1 4.05.06.0
×\times
WW — Форма [3, 2]
Строка Выбранный столбец правой матрицы: Столбец 0 Столбец 1
0 1.02.0
1 0.01.0
2 2.00.0
==
CC — Форма [2, 2]
Строка Столбец 0Столбец 1
0 7.0 4.0
1 Выбранная ячейка результата: 16.0 13.0

Накопите три произведения по внутреннему индексу

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

  1. Произведение, добавляемое к сумме по внутренней оси: Слагаемое k=0k=0

    4.01.0=4.04.0\cdot1.0=4.0

    Произведение
    4.0
    Накопленная сумма
    4.0
  2. Произведение, добавляемое к сумме по внутренней оси: Слагаемое k=1k=1

    5.00.0=0.05.0\cdot0.0=0.0

    Произведение
    0.0
    Накопленная сумма
    4.0
  3. Произведение, добавляемое к сумме по внутренней оси: Слагаемое k=2k=2

    6.02.0=12.06.0\cdot2.0=12.0

    Произведение
    12.0
    Накопленная сумма
    16.0

Транспонируйте логически и повторно используйте веса без копирования

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

Хранимая форма
[2, 3]
Логическая форма
[3, 2]
Форма результата
[2, 2]
Значения результата
[7.0, 4.0, 16.0, 13.0]
  1. Общий пакет весов, повторно используемый благодаря согласованию форм: Пакет результата 0
    Левый пакет
    0
    Правый пакет
    0
    Значения
    [7.0, 4.0, 16.0, 13.0]
  2. Общий пакет весов, повторно используемый благодаря согласованию форм: Пакет результата 1
    Левый пакет
    1
    Правый пакет
    0
    Значения
    [4.0, 1.0, 2.0, 5.0]

Отклоните несовпадающие внутренние и пакетные размеры

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

  1. Отклонённое матричное умножение: inner-dimension-mismatch Размеры двух внутренних осей должны совпадать. 343\ne4
  2. Отклонённое матричное умножение: incompatible-batch Пакетные размеры должны совпадать либо один из них должен быть единичным. Ось пакета 0: 232\ne3

Выбранная ячейка наглядно подтверждает весь расчёт: одна строка и один столбец дают ровно три произведения, а последняя накопленная сумма равна 1616. Затем карточки пакетов показывают, что начальная ось пакета позволяет повторно использовать матрицу весов. Два примера с ошибками показывают различие между равенством внутренних размеров и согласованием форм по начальным осям пакета.

Предскажите произведения до запуска Rust

Запишите эффективные формы до вычисления значений.

  1. Предскажите форму результата и все четыре значения для матрицы AA из примера с формой [2,3], умноженной на WW с формой [3,2].
  2. Повторно вычислите C1,0C_{1,0} в порядке возрастания kk. Какая ось исчезает и почему?
  3. Сохраните W𝖳W^{\mathsf T} с формой [2,3]. Какой флаг восстанавливает логическую форму [3,2] и какой результат остаётся неизменным?
  4. Для левой формы [2,2,3] и правой формы [1,3,2] сопоставьте пакет результата 1 с координатами пакетов обоих входов и предскажите четыре его значения.
  5. Предскажите произведение форм [2,0] и [0,2]. Почему результат не является ошибкой?
  6. Назовите первую ошибку для одномерного левого входа, для внутренних размеров 3 и 4, а затем для совместимых матричных осей с начальными пакетными размерами 2 и 3.
  7. Для непрерывной AA с шагами [3,1] и WW с шагами [2,1] запишите оба плана базовых смещений ячеек и три пары смещений в исходных хранилищах, используемые для C1,0C_{1,0}. Почему тот же алгоритм может работать со срезом, который не является непрерывным в памяти, без предварительного копирования?
  8. Проверьте понимание: можно ли считать матричное умножение поэлементным умножением с одной общей суммой? Укажите разные индексы строки, столбца и внутренней оси.
Проверьте восемь предсказаний о матричном умножении
  1. Совпадающая внутренняя ось не входит в форму результата. Из двух внешних размеров образуется форма [2,2], а значения в построчном порядке равны [7,4,16,13].
  2. 414\cdot1 даёт накопленную сумму 44; 505\cdot0 оставляет её равной 44; 626\cdot2 увеличивает её до 1616. Ось kk исчезает, потому что все три совпадающие позиции вносят вклад в одну ячейку.
  3. Установите transpose_right=true. Хранимая форма [2,3] интерпретируется как логическая [3,2], а результат сохраняет форму [2,2] и значения [7,4,16,13].
  4. Пакет результата 1 сопоставляется с левым пакетом 1 и правым пакетом 0. Его произведение имеет форму [2,2] и значения [4,1,2,5].
  5. Форма [2,2] содержит четыре значения положительного нуля. Каждая ячейка существует, но при K=0K=0 в накопленную сумму, инициализированную как 0.0, не поступает ни одного слагаемого.
  6. Ошибки: сначала LeftRankTooSmall { rank: 1 }, затем InnerDimensionMismatch { left: 3, right: 4 }, затем IncompatibleBatch { axis: 0, left_dimension: 2, right_dimension: 3 }.
  7. План базовых смещений левого операнда равен [3,0], а правого — [0,1]. Для C1,0C_{1,0} начальные смещения равны 3 и 0; шаги 1 и 2 по общей внутренней оси дают пары (3,0), (4,2) и (5,4). Срез передаёт тому же планировщику собственное проверенное базовое смещение и собственные шаги, поэтому память выделяется только для нового непрерывного результата.
  8. Нет. Строка результата ii выбирает одну строку левой матрицы, столбец результата jj — один столбец правой, а внутренний индекс kk сопоставляет значения в общих позициях, перемножает их и складывает произведения. Другие пары строк и столбцов образуют другие ячейки, а не попадают в одну общую сумму.

Подготовьте обучаемые проекции и внимание

Теперь тензорное ядро может умножать двумерные и пакетные представления с произвольными шагами, требовать равенства внутренних размеров, согласовывать формы только по начальным осям пакета и интерпретировать необязательное транспонирование последних осей без копирования. Формы проверяются, а память выделяется с обработкой ошибок до начала скалярных вычислений; успешный результат возвращается как тензор с собственным непрерывным хранилищем.

Общедоступный поиск по координате по-прежнему проверяет каждую координату, переданную вызывающим кодом. Внутри уже проверенного матричного умножения два курсора базовых смещений ячеек и два шага по общей внутренней оси указывают, какой пакет, какую строку, какой столбец и какую позицию kk нужно выбрать, не создавая координаты заново для каждого скалярного произведения.

Именно это численное суммирование произведений по общей оси лежит в основе последующих обучаемых проекций, оценок внимания QK𝖳QK^{\mathsf T}, сумм значений с весами внимания, сетей прямого распространения и их градиентов. Само по себе оно ещё не является обучаемым слоем или механизмом внимания: здесь нет параметров, смещения, маскирования, масштабирования, нормализации или графа обучения.

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