Напишите операцию numpy einsum как собственные тензоры

Я хочу написать следующий numpy einsum в качестве операции Eigen Tensor.

import numpy as np

L = np.random.rand(2, 2, 136)
U = np.random.rand(2, 2, 136)

result = np.einsum('ijl,jkl->ikl', U, L)

Я могу написать это с помощью циклов for на C++

  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 2; j++) {
      for (int k = 0; k < 2; k++) {
        for (int l = 0; l < 136; l++) {
          result(i, k, l) += U(i, j, l) * L(j, k, l);
        }
      }
    }
  }

Как мне написать в собственной нотации, используя его операции? Использование циклов for не позволяет eigen правильно векторизовать операции, так как у меня сложные скалярные типы.

Редактировать.

Как и просили, Jet представляет собой расширение двойных чисел, где каждый элемент представляет собой число, за которым следует массив градиентов этого числа по некоторым параметрам. http://ceres-solver.org/automatic_derivatives.html

Наивная реализация может выглядеть так

template<typename T, int N>
struct Jet
{
    T a;
    T v[N];
};

Если струя написана с использованием eigen ops, идея состоит в том, что с помощью шаблонов выражений eigen должен напрямую векторизовать все операции.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
0
209
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Минимальный рабочий пример

Вот рабочий пример. См. godbolt.org, чтобы запустить код.

#include <Eigen/Dense>
#include <unsupported/Eigen/CXX11/Tensor>

int main() {
    // Setup tensors
    Eigen::Tensor<double, 3> U(2, 2, 136);
    Eigen::Tensor<double, 3> L(2, 2, 136);

    // Fill with random vars
    U.setRandom();
    L.setRandom();

    // Create a vector of dimension pairs you want to contract over
    // Since j is the second index in the second tensor (U) we specify index 1, since j is
    // the first index for the second tensor (L), we specify index 0.
    Eigen::array<Eigen::IndexPair<int>, 1> contraction_dims = {Eigen::IndexPair<int>(1,0)};

    // Perform contraction and save result
    Eigen::Tensor<double, 3> result = U.contract(L, contraction_dims);

}

Векторизация

Векторизация — сложная штука. Скорее всего, вы захотите скомпилировать код с помощью -O3 -fopt-info-vec-missed, где -fopt-info-vec-missed выведет очень подробную информацию о том, какие векторизации были пропущены. Если вам действительно нужна дополнительная информация о том, почему ваш компилятор оптимизирует/не оптимизирует вещи так, как вы надеялись, ознакомьтесь с такими инструментами проверки, как optview2 и это отличное выступление от CPPCON Офека Шилона. Надеюсь это поможет.

Это не правильное сокращение.

Niteya Shah 17.01.2023 23:23
Ответ принят как подходящий

В вашем случае не происходит сжатия в 3-м измерении «l». Итак, в каком-то смысле L и U — это массивы матриц 2x2 длиной 136, и вы умножаете матрицу U[l] на L[l]. Я думаю, что сделать что-то подобное np.einsum с Eigen невозможно; Eigen::Tensor::contract поддерживает только «настоящие» сокращения. Но, конечно, всегда можно выполнить цикл по 3-му измерению вручную. Но, как показано ниже, это работает очень плохо.

Тем не менее, есть способы ускорить работу и векторизовать циклы, либо полагаясь на автоматическую векторизацию (у меня это не сработало), либо давая дополнительные подсказки компилятору (через OpenMP SIMD).

Далее я определяю cDim12=2 как размер первого и второго измерения, а cDim13=136 как третьего измерения. Что касается таймингов, весь код был скомпилирован с помощью -O3 -mavx с gcc 11.2 и clang 15.0.2. Я использовал бенчмарк Google, чтобы узнать тайминги на Intel Core i7-4770K (да, ему уже несколько лет, извините). Был использован ствол Eigen (08c961e83) от 20 января 2023 года.

TL;DR: подведем итоги ниже:

  • Использование ручного цикла (с лучшим порядком итерации), автоматической векторизации с помощью прагмы OpenMP-SIMD, а также прямого доступа с помощью gcc было самым быстрым («DirectAccessWithOMP»): в 2,8 раза быстрее, чем ваш простой цикл с AVX. Я думаю, что это близко к «оптимальному» (ср. Godbolt).
  • Я не мог заставить clang правильно векторизовать цикл. Поскольку вы упомянули, что подходит либо gcc, либо clang, у вас, похоже, есть выбор, и я бы остановился на gcc.
  • Python кажется на порядок или более медленнее по сравнению с самым быстрым результатом gcc.

Примечание. Измерьте свое приложение в реальном мире, так как там все может вести себя совершенно по-другому!

Код из оригинального поста в качестве основы ("FromOriginalPost")

Простой код из вашего исходного поста выглядит так и используется в качестве основы.

Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
result.setZero();
for (int i = 0; i < cDim12; i++) {
  for (int j = 0; j < cDim12; j++) {
    for (int k = 0; k < cDim12; k++) {
      for (int l = 0; l < cDim3; l++) {
        result(i, k, l) += U(i, j, l) * L(j, k, l);
      }
    }
  }
}

Оптимизированный порядок циклов ("OptimizedOrder")

Обратите внимание, что Eigen::Tensor по умолчанию использует основной порядок столбцов (и основной порядок строк не рекомендуется). Таким образом, в таком выражении, как U(i, j, l), i должен быть самым быстрым (самым внутренним) циклом, а l — самым медленным (самым внешним) циклом. Переупорядочиваю как могу:

for (int l = 0; l < cDim3; l++) {
  for (int j = 0; j < cDim12; j++) {
    for (int k = 0; k < cDim12; k++) {
      for (int i = 0; i < cDim12; i++) {
        result(i, k, l) += U(i, j, l) * L(j, k, l);
      }
    }
  }
}

Это в 1,3-1,4 раза быстрее.

Использование Eigen::Tensor::chip и contract ("EigenChipAndContract")

Максимально используя функции Eigen, я пришел к следующему:

Eigen::array<Eigen::IndexPair<int>, 1> productDims = {Eigen::IndexPair<int>(1, 0)};
Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
for (int l = 0; l < cDim3; l++) {
  result.chip(l, 2) = U.chip(l, 2).contract(L.chip(l, 2), productDims);
}

Это работает очень плохо: оно в 18 раз медленнее на gcc и в 24 раза медленнее на clang по сравнению с «FromOriginalPost».

Использование Eigen::TensorMap и contract ("EigenMapAndContract")

«EigenChipAndContract» может много копировать, поэтому другая идея заключалась в том, чтобы использовать Eigen::TensorMap для получения «ссылок» на каждый необходимый «фрагмент» данных. Для доступа к необработанному массиву снова обратите внимание, что Eigen использует порядок столбцов.

Eigen::array<Eigen::IndexPair<int>, 1> productDims = {Eigen::IndexPair<int>(1, 0)};
Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
for (int l = 0; l < cDim3; l++) {
  Eigen::TensorMap<Eigen::Tensor<double, 2>> U_chip(U.data() + l * cDim12 * cDim12, cDim12, cDim12);
  Eigen::TensorMap<Eigen::Tensor<double, 2>> L_chip(L.data() + l * cDim12 * cDim12, cDim12, cDim12);
  Eigen::TensorMap<Eigen::Tensor<double, 2>> result_chip(result.data() + l * cDim12 * cDim12, cDim12, cDim12);
  result_chip = U_chip.contract(L_chip, productDims);
}

На самом деле это несколько быстрее, чем "EigenChipAndContract", но все же очень медленно. По сравнению с FromOriginalPost, это в 14 раз медленнее для gcc и в 19 раз медленнее для clang.

Векторизация с OpenMP ("EigenAccessWithOMP")

Хотя и gcc , и clang умеют делать автоматическую векторизацию, без дополнительных подсказок они не дают хороших результатов. Однако оба поддерживают прагму OpenMP #pragma omp simd collap(4) при компиляции с -fopenmp:

#pragma omp simd collapse(4)
for (int l = 0; l < cDim3; l++) {
  for (int j = 0; j < cDim12; j++) {
    for (int k = 0; k < cDim12; k++) {
      for (int i = 0; i < cDim12; i++) {
        result(i, k, l) += U(i, j, l) * L(j, k, l);
      }
    }
  }
}

Компиляция с -O3 -mavx -fopenmp приводит к

  • время выполнения в 2,5 раза быстрее по сравнению с исходным кодом («FromOriginalPost») для gcc,
  • но для clang код в 2,5 раза медленнее. Поиск проблем clang + OpenMP-SIMD, по-видимому, у clang иногда возникают проблемы (например, в этом посте). Проверяя результат на godbolt, clang действительно выдает довольно длинные результаты.

Векторизация с OpenMP + прямой необработанный доступ ("DirectAccessWithOMP")

В предыдущем коде использовалась Eigen::Tensor::operator(), которая должна быть встроена в необработанный доступ к массиву. Однако, помня о расположении столбцов, мы также можем получить прямой доступ к базовому массиву и проверить, улучшает ли это что-либо. Это также позволяет снова дать компилятору подсказку, что данные правильно выровнены (хотя Eigen уже определяет их как таковые).

double * pR = result.data();
double * pU = U.data();
double * pL = L.data();

#pragma omp simd collapse(4) aligned(pR, pU, pL: 32) // 32: For AVX
for (int l = 0; l < cDim3; l++) {
  for (int j = 0; j < cDim12; j++) {
    for (int k = 0; k < cDim12; k++) {
      for (int i = 0; i < cDim12; i++) {
        pR[i + cDim12*(k + cDim12*l)] += pU[i + cDim12*(j + cDim12*l)] * pL[j + cDim12*(k + cDim12*l)];
      }
    }
  }
}

Несколько удивительно, что это в 1,1 раза быстрее для gcc и в 1,4 раза быстрее для clang по сравнению с «EigenAccessWithOMP». По сравнению с оригинальным «FromOriginalPost» он в 2,8 раза быстрее для gcc и в 2,5 раза медленнее для clang.

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

питон

Не уверен, насколько неправдоподобно сравнивать абсолютное время выполнения вызова np.einsum с версией C++, поскольку Python должен выполнять дополнительный анализ строк и т. д. Тем не менее, вот код:

import numpy as np
import timeit

L = np.random.rand(2, 2, 136)
U = np.random.rand(2, 2, 136)

numIterations = 1000000
timing = timeit.timeit(lambda: np.einsum('ijl,jkl->ikl', U, L), number=numIterations)
print(f"np.einsum (per iteration): {timing.real/(numIterations*1e-9)}ns")

Для Python 3.9 и numpy-1.24.1 это примерно в 6 раз медленнее по сравнению с «FromOriginalPost» и в 16 раз медленнее по сравнению с «DirectAccessWithOMP» для gcc.

Сырые тайминги

Для gcc:

---------------------------------------------------------------
Benchmark                     Time             CPU   Iterations
---------------------------------------------------------------
FromOriginalPost            823 ns          823 ns      3397793
OptimizedOrder              573 ns          573 ns      4895246
DirectAccess               1306 ns         1306 ns      2142826
EigenAccessWithOMP          324 ns          324 ns      8655549
DirectAccessWithOMP         296 ns          296 ns      9418635
EigenChipAndContract      14405 ns        14405 ns       193548
EigenMapAndContract       11390 ns        11390 ns       243122

Для лязга:

---------------------------------------------------------------
Benchmark                     Time             CPU   Iterations
---------------------------------------------------------------
FromOriginalPost            753 ns          753 ns      3714543
OptimizedOrder              570 ns          570 ns      4921914
DirectAccess                569 ns          569 ns      4929755
EigenAccessWithOMP         2704 ns         2704 ns      1037819
DirectAccessWithOMP        1908 ns         1908 ns      1466390
EigenChipAndContract      17713 ns        17713 ns       157427
EigenMapAndContract       14064 ns        14064 ns       198875

Питон:

np.einsum (per iteration): 4873.6035999991145 ns

Полный код

Также на godbolt, однако это не очень полезно, так как компилятор довольно часто отключается. Локально я скомпилировал с помощью -O3 -DNDEBUG -std=c++17 -mavx -fopenmp -Wall -Wextra.

#include <iostream>
#include <iomanip>
#include <cmath>

#include <unsupported/Eigen/CXX11/Tensor>
#include <benchmark/benchmark.h>

//====================================================
// Globals
//====================================================

static constexpr int cDim12 = 2;
static constexpr int cDim3 = 136;


Eigen::Tensor<double, 3> CreateRandomTensor()
{
  Eigen::Tensor<double, 3> m(cDim12, cDim12, cDim3);
  m.setRandom();
  return m;
}


Eigen::Tensor<double, 3> const L = CreateRandomTensor();
Eigen::Tensor<double, 3> const U = CreateRandomTensor();


//====================================================
// Helpers
//====================================================

Eigen::Tensor<double, 3> ReferenceResult() 
{
  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  result.setZero();
  for (int i = 0; i < cDim12; i++) {
    for (int j = 0; j < cDim12; j++) {
      for (int k = 0; k < cDim12; k++) {
        for (int l = 0; l < cDim3; l++) {
          result(i, k, l) += U(i, j, l) * L(j, k, l);
        }
      }
    }
  }

  return result;
}



void CheckResult(Eigen::Tensor<double, 3> const & result) 
{
  Eigen::Tensor<double, 3> const ref = ReferenceResult();
  Eigen::Tensor<double, 3> const diff = ref - result;
  Eigen::Tensor<double, 0> const max = diff.maximum();
  Eigen::Tensor<double, 0> const min = diff.minimum();
  double const maxDiff = std::max(std::abs(max(0)), std::abs(min(0)));
  if (maxDiff > 1e-14) {
    std::cerr << "ERROR! Max Diff = " << std::setprecision(17) << maxDiff << std::endl;
  }
}


//====================================================
// Benchmarks
//====================================================


static void FromOriginalPost(benchmark::State& state) 
{
  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    result.setZero();
    for (int i = 0; i < cDim12; i++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int l = 0; l < cDim3; l++) {
            result(i, k, l) += U(i, j, l) * L(j, k, l);
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }

  CheckResult(result);
}
BENCHMARK(FromOriginalPost);


static void OptimizedOrder(benchmark::State& state) 
{
  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    result.setZero();
    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            result(i, k, l) += U(i, j, l) * L(j, k, l);
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }

  CheckResult(result);
}
BENCHMARK(OptimizedOrder);


static void DirectAccess(benchmark::State& state) 
{
  Eigen::Tensor<double, 3> U = ::U;
  Eigen::Tensor<double, 3> L = ::L;

  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    result.setZero();
    double * pR = result.data();
    double * pU = U.data();
    double * pL = L.data();

    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            pR[i + cDim12*(k + cDim12*l)] += pU[i + cDim12*(j + cDim12*l)] * pL[j + cDim12*(k + cDim12*l)];
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }

  CheckResult(result);
}
BENCHMARK(DirectAccess);


static void EigenAccessWithOMP(benchmark::State& state) 
{
  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    result.setZero();
    #pragma omp simd collapse(4)
    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            result(i, k, l) += U(i, j, l) * L(j, k, l);
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }

  CheckResult(result);
}
BENCHMARK(EigenAccessWithOMP);


static void DirectAccessWithOMP(benchmark::State& state) 
{
  Eigen::Tensor<double, 3> U = ::U;
  Eigen::Tensor<double, 3> L = ::L;

  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    result.setZero();
    double * pR = result.data();
    double * pU = U.data();
    double * pL = L.data();

    #pragma omp simd collapse(4) aligned(pR, pU, pL: 32) // 32: For AVX
    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            pR[i + cDim12*(k + cDim12*l)] += pU[i + cDim12*(j + cDim12*l)] * pL[j + cDim12*(k + cDim12*l)];
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }

  CheckResult(result);
}
BENCHMARK(DirectAccessWithOMP);


static void EigenChipAndContract(benchmark::State& state) 
{
  Eigen::array<Eigen::IndexPair<int>, 1> productDims = {Eigen::IndexPair<int>(1, 0)};
  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    result.setZero();
    for (int l = 0; l < cDim3; l++) {
      result.chip(l, 2) = U.chip(l, 2).contract(L.chip(l, 2), productDims);
    }
    benchmark::DoNotOptimize(result.data());
  }

  CheckResult(result);
}
BENCHMARK(EigenChipAndContract);


static void EigenMapAndContract(benchmark::State& state) 
{
  Eigen::Tensor<double, 3> U = ::U;
  Eigen::Tensor<double, 3> L = ::L;
  Eigen::array<Eigen::IndexPair<int>, 1> productDims = {Eigen::IndexPair<int>(1, 0)};

  Eigen::Tensor<double, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    result.setZero();
    for (int l = 0; l < cDim3; l++) {
      Eigen::TensorMap<Eigen::Tensor<double, 2>> U_chip(U.data() + l * cDim12 * cDim12, cDim12, cDim12);
      Eigen::TensorMap<Eigen::Tensor<double, 2>> L_chip(L.data() + l * cDim12 * cDim12, cDim12, cDim12);
      Eigen::TensorMap<Eigen::Tensor<double, 2>> result_chip(result.data() + l * cDim12 * cDim12, cDim12, cDim12);
      result_chip = U_chip.contract(L_chip, productDims);
    }
    benchmark::DoNotOptimize(result.data());
  }

  CheckResult(result);
}
BENCHMARK(EigenMapAndContract);


BENCHMARK_MAIN();


РЕДАКТИРОВАТЬ для самолетов

После того, как исходный пост был отредактирован, используемые арифметические типы на самом деле не являются встроенными, а скорее струями . Eigen можно расширить для поддержки пользовательских типов (как кратко описано здесь). Однако функция Eigen::Tensor::contract(), тем не менее, не поддерживает «волшебным образом» эквивалент np.einsum('ijl,jkl->ikl', U, L), поскольку последнее измерение l на самом деле не выполняет сжатие. Конечно, можно было бы написать один, но это кажется далеко не тривиальным.

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

Тип струи (адаптировано из здесь):

template<int N> struct Jet {
  double a = 0.0;
  Eigen::Matrix<double, 1, N> v = Eigen::Matrix<double, 1, N>::Zero();
};

template<int N> 
EIGEN_STRONG_INLINE Jet<N> operator+(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>{f.a + g.a, f.v + g.v};
}

template<int N> 
EIGEN_STRONG_INLINE Jet<N> operator*(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>{f.a * g.a, f.a * g.v + f.v * g.a};
}

Например (основной столбец)

Eigen::Tensor<Jet<N>, 3> L = CreateRandomTensor<Eigen::ColMajor>();
Eigen::Tensor<Jet<N>, 3> U = CreateRandomTensor<Eigen::ColMajor>();

Eigen::Tensor<Jet<N>, 3> result(cDim12, cDim12, cDim3);
SetToZero(result);

for (int l = 0; l < cDim3; l++) {
  for (int j = 0; j < cDim12; j++) {
    for (int k = 0; k < cDim12; k++) {
      for (int i = 0; i < cDim12; i++) {
        Jet<N> & r = result(i, k, l);
        r = r + U(i, j, l) * L(j, k, l);
      }
    }
  }
}

или с порядком строк:

Eigen::Tensor<Jet<N>, 3, Eigen::RowMajor> L = CreateRandomTensor<Eigen::RowMajor>();
Eigen::Tensor<Jet<N>, 3, Eigen::RowMajor> U = CreateRandomTensor<Eigen::RowMajor>();

Eigen::Tensor<Jet<N>, 3, Eigen::RowMajor> result(cDim12, cDim12, cDim3);
SetToZero(result);

for (int i = 0; i < cDim12; i++) {
  for (int k = 0; k < cDim12; k++) {
    for (int j = 0; j < cDim12; j++) {
      for (int l = 0; l < cDim3; l++) {
        Jet<N> & r = result(i, k, l);
        r = r + U(i, j, l) * L(j, k, l);
      }
    }
  }
}

gcc и clang дают одинаковую производительность. Они автоматически векторизуют основные циклы по столбцам, но, по-видимому, не основные по строкам. Прямой доступ к базовым данным ничего не улучшает. Более того, добавление #pragma omp simd collapse(4) приводит к ухудшению производительности в обоих случаях (clang также предупреждает, что циклы не могут быть векторизованы); Я предполагаю, что явные SIMD, используемые в матричном умножении Jet::v внутри Eigen, являются причиной.

Еще раз в качестве дополнительного примечания: в документации Eigen говорится, что вам не следует комбинировать порядок строк с Eigen::Tensor:

Библиотека тензоров поддерживает 2 макета: ColMajor (по умолчанию) и RowMajor. В настоящее время полностью поддерживается только основной макет столбца по умолчанию, поэтому в настоящее время не рекомендуется пытаться использовать основной макет строки.

Полный код:

#include <iostream>
#include <iomanip>
#include <cmath>

#include <unsupported/Eigen/CXX11/Tensor>
#include <benchmark/benchmark.h>


static constexpr int cDim12 = 2;
static constexpr int cDim3 = 136;


template<int N> struct Jet {
  double a = 0.0;
  Eigen::Matrix<double, 1, N> v = Eigen::Matrix<double, 1, N>::Zero();
};

template<int N> 
EIGEN_STRONG_INLINE Jet<N> operator+(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>{f.a + g.a, f.v + g.v};
}

template<int N> 
EIGEN_STRONG_INLINE Jet<N> operator-(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>{f.a - g.a, f.v - g.v};
}

template<int N> 
EIGEN_STRONG_INLINE Jet<N> operator*(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>{f.a * g.a, f.a * g.v + f.v * g.a};
}

template<int N> 
EIGEN_STRONG_INLINE Jet<N> operator/(const Jet<N>& f, const Jet<N>& g) {
  return Jet<N>{f.a / g.a, f.v / g.a - f.a * g.v / (g.a * g.a)};
}



static constexpr int N = 8;


template <Eigen::StorageOptions storage>
auto CreateRandomTensor()
{
  Eigen::Tensor<Jet<N>, 3, storage> result(cDim12, cDim12, cDim3);
  for (int l = 0; l < cDim3; l++) {
    for (int k = 0; k < cDim12; k++) {
      for (int i = 0; i < cDim12; i++) {
        Jet<N> jet;
        jet.a = (double)rand() / RAND_MAX;
        jet.v.setRandom();
        result(i, k, l) = jet;
      }
    }
  }
  return result;
}


template <class T>
void SetToZero(T & result)
{
  for (int l = 0; l < cDim3; l++) {
    for (int k = 0; k < cDim12; k++) {
      for (int i = 0; i < cDim12; i++) {
        result(i, k, l) = Jet<N>{};
      }
    }
  }
}


static void EigenAccessNoOMP(benchmark::State& state) 
{
  srand(42);
  Eigen::Tensor<Jet<N>, 3> L = CreateRandomTensor<Eigen::ColMajor>();
  Eigen::Tensor<Jet<N>, 3> U = CreateRandomTensor<Eigen::ColMajor>();

  Eigen::Tensor<Jet<N>, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    SetToZero(result);

    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            Jet<N> & r = result(i, k, l);
            r = r + U(i, j, l) * L(j, k, l);
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }
}
BENCHMARK(EigenAccessNoOMP);


static void EigenAccessNoOMPRowMajor(benchmark::State& state) 
{
  srand(42);
  Eigen::Tensor<Jet<N>, 3, Eigen::RowMajor> L = CreateRandomTensor<Eigen::RowMajor>();
  Eigen::Tensor<Jet<N>, 3, Eigen::RowMajor> U = CreateRandomTensor<Eigen::RowMajor>();

  Eigen::Tensor<Jet<N>, 3, Eigen::RowMajor> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    SetToZero(result);

    for (int i = 0; i < cDim12; i++) {
      for (int k = 0; k < cDim12; k++) {
        for (int j = 0; j < cDim12; j++) {
          for (int l = 0; l < cDim3; l++) {
            Jet<N> & r = result(i, k, l);
            r = r + U(i, j, l) * L(j, k, l);
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }
}
BENCHMARK(EigenAccessNoOMPRowMajor);



static void DirectAccessNoOMP(benchmark::State& state) 
{
  srand(42);
  Eigen::Tensor<Jet<N>, 3> L = CreateRandomTensor<Eigen::ColMajor>();
  Eigen::Tensor<Jet<N>, 3> U = CreateRandomTensor<Eigen::ColMajor>();

  Eigen::Tensor<Jet<N>, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    SetToZero(result);

    Jet<N> * pR = result.data();
    Jet<N> * pU = U.data();
    Jet<N> * pL = L.data();

    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            Jet<N> & r = pR[i + cDim12*(k + cDim12*l)];
            r = r + pU[i + cDim12*(j + cDim12*l)] * pL[j + cDim12*(k + cDim12*l)];
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }
}
BENCHMARK(DirectAccessNoOMP);


static void EigenAccessWithOMP(benchmark::State& state) 
{
  srand(42);
  Eigen::Tensor<Jet<N>, 3> L = CreateRandomTensor<Eigen::ColMajor>();
  Eigen::Tensor<Jet<N>, 3> U = CreateRandomTensor<Eigen::ColMajor>();

  Eigen::Tensor<Jet<N>, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    SetToZero(result);

    #pragma omp simd collapse(4)
    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            Jet<N> & r = result(i, k, l);
            r = r + U(i, j, l) * L(j, k, l);
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }
}
BENCHMARK(EigenAccessWithOMP);



static void DirectAccessWithOMP(benchmark::State& state) 
{
  srand(42);
  Eigen::Tensor<Jet<N>, 3> L = CreateRandomTensor<Eigen::ColMajor>();
  Eigen::Tensor<Jet<N>, 3> U = CreateRandomTensor<Eigen::ColMajor>();

  Eigen::Tensor<Jet<N>, 3> result(cDim12, cDim12, cDim3);
  for (auto _ : state) {
    SetToZero(result);

    Jet<N> * pR = result.data();
    Jet<N> * pU = U.data();
    Jet<N> * pL = L.data();

    #pragma omp simd collapse(4) aligned(pR, pU, pL: 32)
    for (int l = 0; l < cDim3; l++) {
      for (int j = 0; j < cDim12; j++) {
        for (int k = 0; k < cDim12; k++) {
          for (int i = 0; i < cDim12; i++) {
            Jet<N> & r = pR[i + cDim12*(k + cDim12*l)];
            r = r + pU[i + cDim12*(j + cDim12*l)] * pL[j + cDim12*(k + cDim12*l)];
          }
        }
      }
    }

    benchmark::DoNotOptimize(result.data());
  }
}
BENCHMARK(DirectAccessWithOMP);



BENCHMARK_MAIN();

Так что не используйте clang?

Mad Physicist 21.01.2023 21:08

@MadPhysicist В этом очень конкретном случае я бы сказал да: используйте gcc вместо clang. gcc здесь работает намного лучше. Но это действительно зависит от приложения. У меня также был другой (совершенно несвязанный) код, где clang лучше, чем gcc, оптимизировал его.

Sedenion 21.01.2023 21:12

Мой порядок строк уже был оптимизирован для RowMajor, поскольку данные создаются из предыдущей операции, предназначенной для основных массивов строк. Причина, по которой я хочу использовать собственные выражения, заключается в том, что мой тип данных - это не арифметические типы, а струйные, и поэтому подобные оптимизации simd не применяются. Я надеялся использовать собственные выражения для автоматической оптимизации операции.

Niteya Shah 22.01.2023 02:50

@NiteyaShah Я не понимаю: Эйген может использовать векторизацию только для встроенных арифметических типов. Если вы используете какие-то другие типы, скажем, какую-то структуру, неизвестную Eigen, как вы ожидаете, что Eigen будет векторизовать это? Кроме того, ваш код numpy использует плавающие точки. И что вы подразумеваете под "струей"? Поэтому, пожалуйста, обновите свой вопрос и объясните, что у вас на самом деле есть и что вы хотите. Кажется, вы на самом деле не после векторизации (т.е. использования SIMD)?

Sedenion 22.01.2023 09:46

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

Niteya Shah 23.01.2023 00:50

@NiteyaShah Я не думал, что вы готовы зайти так далеко, чтобы расширить Eigen до пользовательских типов. Однако Eigen::Tensor::contract по-прежнему не поддерживает операцию 'ijl,jkl->ikl' волшебным образом. Так что вам все равно нужно реализовать это самостоятельно. Или, если это единственная операция Eigen, которая вам действительно нужна, все же проще реализовать ее через циклы и поиграться с компиляторами, настройками и т. д. gcc и clang векторизуют циклы. Я исправил свой ответ с примером. Или вы делаете гораздо больше вещей с тензорами, так что расширение Eigen действительно стоит затраченных усилий?

Sedenion 23.01.2023 10:07

У меня нет проблем с разработкой оптимального метода для этого с использованием простых циклов for. Я даже могу развернуть внутренний цикл Jets и выполнить оптимальные SIMD-операции. К сожалению, это противоречит цели использования eigen. Я должен поддерживать несколько архитектур (x86-64, arm, nvidia и amd), и eigen должен обеспечивать минимальные усилия по переводу между ними. У меня нет проблем с расширением собственных выражений для поддержки этого метода, поскольку в будущем у нас возникнут аналогичные проблемы, которые нам необходимо поддерживать.

Niteya Shah 23.01.2023 16:04

@NiteyaShah Хорошо. Но чего вы ожидаете в качестве ответа на stackoverflow? Готовое расширение Eigen для струй и дополнительно обобщение Eigen::Tensor::contract? Сомневаюсь, что кто-то захочет сделать эту работу за вас. Или ответ на ваш первоначальный вопрос просто «в Эйгене нет ничего подобного np.einsum('ijl,jkl->ikl', U, L)»?

Sedenion 23.01.2023 19:38

@Sedenion Мне не нужно расширение eigen для использования струй. Как задается вопрос, мне нужна реализация функции, которая использует собственные выражения (а не циклы for) для создания оптимального кода для этого einsum. Если вы можете создать функцию, используя собственные выражения даже для обычных типов данных (двойных или с плавающей запятой), которые правильно векторизованы, я могу с этим работать. Как вы видели в своей реализации с использованием микросхем, это не работает. Мне не нужна ручная оптимизация для циклов.

Niteya Shah 23.01.2023 19:57

@NiteyaShah Единственная другая идея, которая у меня есть, - рассматривать ваш тензор как 2*136 x 2*136 разреженную матрицу: для каждого l в U(i,j,l)i,j образует матрицу 2x2. Поместите их по диагонали большой матрицы 2*136 x 2*136. Таким образом, диагональ представляет собой матрицы 2x2. Сделайте то же самое для L. Затем вы можете просто умножать оба друг на друга, а результат (и особенно память) можно напрямую сопоставить обратно с тензором. Он дает тот же результат, но лишь немного быстрее, чем "EigenMapAndContract", и в 30 раз медленнее, чем "DirectAccessWithOMP". Извините, нет идей.

Sedenion 24.01.2023 22:38

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

Sedenion 24.01.2023 22:41

К сожалению, PR-агенты какое-то время не видели никаких действий.

Niteya Shah 26.01.2023 16:13

Другие вопросы по теме