В прошлом уроке матрица была движением пространства. Теперь спросим приземлённее: во что это движение обходится. У большой сети почти всё время уходит на умножение матриц, и от его цены зависит, какую модель вообще можно обучить. Причём цена прячется в двух местах: в числе арифметических операций и в том, как данные ходят по памяти.
Три вложенных цикла
Чтобы перемножить и , каждый из элементов результата собирают как скалярное произведение строки на столбец:
Это умножений и примерно столько же сложений. Для квадратных матриц получается порядка операций.
Восьмикратный рост при удвоении стороны — вот что делает большие матрицы дорогими. Но у той же формулы есть скрытая степень свободы, которая меняет цену, не трогая ответ.
Порядок скобок меняет цену
Умножение матриц ассоциативно: , ответ один. А вот стоимость двух порядков может отличаться в разы. Возьмём цепочку , , .
Разница почти в девять раз — на ровном месте, просто из-за расстановки скобок.

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

Реальные измерения на этой машине. Оптимизированная библиотека даже обгоняет чистый : её точки уходят ниже эталонной линии, потому что она распараллеливает работу и лучше использует память. Наивная же реализация даёт наклон ровно три.
Блоки и кэш
Спасение от лишних чтений — блочное умножение. Матрицы режут на плитки , которые целиком помещаются в быструю память. Загруженная плитка используется много раз, прежде чем её вытеснят:
Арифметика не меняется, а обмен с медленной памятью резко падает.
Вопреки распространённому мнению, число арифметических операций, необходимых для умножения двух матриц, растёт медленнее, чем куб их размера.
Поэтому на практике берут порог: выше него — рекурсия Штрассена, ниже — оптимизированное обычное умножение. Слишком ранняя рекурсия проигрывает из-за временных матриц и лишних сложений.
Показатель степени у Штрассена () меньше, чем у обычного метода (). Почему же на матрице он часто оказывается медленнее?
Показать ответ
Меньший показатель побеждает лишь асимптотически, на больших . На малых размерах решает постоянный множитель: каждый уровень рекурсии добавляет сложений и вычитаний блоков и заводит временные матрицы, а сама рекурсия тратит память и мешает кэшу. На этот накладной расход перевешивает экономию одного умножения из восьми. Именно поэтому вводят порог и ниже него считают обычным методом.
Точность быстрого умножения
За скорость Штрассен платит и точностью: вычитания близких чисел усиливают относительную ошибку округления. Поэтому быстрый алгоритм нельзя принимать на веру — результат сверяют с эталоном повышенной точности по относительной норме:
Один случайный пример с равномерными числами не проверяет устойчивость: трудные тесты — это матрицы с числами очень разных масштабов и почти взаимной компенсацией.
Обозначение говорит лишь о поведении на бесконечности; на конкретных размерах решает постоянный множитель, и его нельзя игнорировать при выборе алгоритма.
Свёртка как умножение матриц
Умножение матриц — не абстракция: в него сворачивается сама свёртка из
урока 28. Приём im2col превращает каждое локальное окно картинки в
строку матрицы, а ядра — в столбцы; одно матричное умножение сразу выдаёт все
позиции и каналы. Платят за это временной памятью: пиксели окон повторяются.

Для слоя с ядром матрица im2col имеет строки
и столбца на изображение — заметно больше исходных значений. Именно
поэтому архитектуру CNN оценивают по двум числам сразу: параметрам и
операциям, ведь один вес свёртки применяется во многих позициях.
Вход , ядро , паддинг сохраняет размер, выход
канала. Оцените число умножений на одно изображение через GEMM после im2col.
Показать ответ
Матрица im2col имеет по одной строке на каждую из выходных
позиций и по столбца (окно на все входные каналы). Матрица ядер
— . GEMM делает млн умножений. Это и
есть цена одного слоя на одну картинку; на пакет из — умножаем на .
::::
Быстрый алгоритм умножения — это по сути малоранговое разложение самого вычисления: находя скрытую структуру, мы заменяем много операций немногими.
Русская линия здесь ведёт к тензорным методам. Академик Е. Е. Тыртышников и его школа развивают малоранговые и тензорные разложения (в том числе тензорный поезд), которые позволяют работать с матрицами и данными огромной размерности, недоступными прямому кубическому умножению. Поиск быстрых произведений и тензорные разложения — две стороны одной идеи: заменить лобовой счёт учётом скрытой структуры.
Матрицы в обучении
При обучении сети мало одного : обратный проход из урока 25 требует ещё двух умножений на транспонированные матрицы, и . Поэтому один полносвязный слой запускает несколько GEMM за шаг, и ускорение только прямого прохода не ускоряет эпоху целиком. Цена матриц — это цена обучения.
Отсюда практический вывод: считать надо всю эпоху, а не отдельное умножение. Прямой проход, обратный проход и обновление весов вместе определяют, сколько шагов поместится в отведённое время, а размеры пакета и слоёв подбирают так, чтобы матрицы ложились на аппаратные блоки ускорителя без остатка. Пустые края и неудобные размеры оставляют вычислительные блоки простаивать, и формально одинаковое число операций растягивается во времени. Поэтому инженер смотрит не только на показатель степени в асимптотике, но и на то, насколько плотно реальные размеры загружают железо.
Что забрать с собой
Умножение матриц стоит порядка операций, и на нём держится время обучения.
Но цена прячется в двух местах: в арифметике и в движении данных по памяти, поэтому
её мерят, а не только считают. Ассоциативность даёт бесплатную экономию — правильный
порядок скобок меняет цену в разы. Штрассен перебивает кубический показатель, но
лишь после порога и ценой точности. А свёртка через im2col сводится к тому же
матричному умножению, связывая эту главу с CNN и
обратным распространением.
Задачи
Классический алгоритм вычисляет скалярными произведениями. Для размера и размера найдите число умножений и сложений, считая сумму длины за сложений. Повторите для размеров на и для на . Для каждого случая приведите отношение к исходному числу операций и объясните, почему удвоение всех трёх размеров и удвоение только внутреннего размера дают разные множители.
Для цепочки матриц с размерами выведите общие формулы стоимости и в числах умножений. При найдите, при каком значении оба порядка стоят поровну, и покажите, что при выгоден один порядок, а при большом — другой. Объясните правило «сначала уничтожай большую общую размерность».
Квадратные матрицы увеличили сторону вдвое, с до . Во сколько раз выросли: число элементов результата, число скалярных умножений и минимальный объём чтения входных данных (в элементах)? Почему первые две величины растут по-разному, и как это связано с понятием арифметической интенсивности — отношения операций к прочитанным байтам?
Рекуррентность Штрассена на каждом уровне добавляет сложений блоков размера . Объясните, почему при малых (скажем, ) Штрассен может проигрывать обычному умножению, несмотря на меньший показатель степени. Оцените, как выбор порога рекурсии влияет на итоговое время, и почему порог подбирают экспериментально, а не из одной асимптотики.
Матрицы размера хранятся построчно (row-major) в float64; строка
кэша равна байтам и вмещает восемь соседних чисел. Считайте только чтения :
кэш пуст, полностью ассоциативен, вмещает ровно одну строку кэша, LRU, без
предвыборки. Для порядков
for i: for j: for k: C[i,j]+=A[i,k]*B[k,j] и
for i: for k: for j: C[i,j]+=A[i,k]*B[k,j]
выпишите при последовательность линейных индексов , номера строк кэша и
hit/miss. Найдите общее число промахов за все четыре и объясните различие через
расположение в памяти и повторное использование.
Для слоя im2col: вход , ядро , шаг ,
паддинг сохраняет размер, выход каналов. Выведите размеры матрицы
im2col и матрицы ядер, число умножений в результирующем GEMM и коэффициент
раздувания памяти (отношение числа значений в im2col к числу входных значений).
Подставьте , , , и сравните
временную память с исходными значениями.
Для каждого один раз с seed 17 сгенерируйте float64-матрицы
. Реализуйте порядки ijk, ikj и тройное блочное
умножение без matmul, dot, einsum или @; для блоков
пропускайте . Эталон — numpy.matmul(A,B); критерий
корректности .
Зафиксируйте один поток, три неучитываемых прогрева и запусков; измеряйте только
умножение. Приведите медиану и интервал –, ускорение относительно ijk и
лучший для каждого ; объясните, почему лучший зависит от машины.
Реализуйте float64-Штрассен для (дополняя нулями до
степени двойки) с переходом на numpy.matmul при пороге .
Для каждого и seed 17 постройте два теста: и «трудный»
, с диагоналями показателей
. Для всех возьмите повышенной
точности и измерьте относительную ошибку Штрассена и обычного numpy.matmul. Время
измеряйте после трёх прогревов по медиане семи запусков при одном потоке BLAS.
Найдите точку перелома — минимальный , где лучший порог быстрее numpy.matmul, —
и укажите модель процессора, версии библиотек и число потоков.