Tensor: Handle oversized GPU convolution kernels

libeigen/eigen!3438

Closes #2762
diff --git a/contrib/Eigen/src/Tensor/TensorConvolution.h b/contrib/Eigen/src/Tensor/TensorConvolution.h
index 70357aa..8dab047 100644
--- a/contrib/Eigen/src/Tensor/TensorConvolution.h
+++ b/contrib/Eigen/src/Tensor/TensorConvolution.h
@@ -653,6 +653,40 @@
   }
 }
 
+// Oversized kernels cannot stage even one output's input footprint in shared memory.
+template <typename InputEvaluator, typename Index, typename InputDims>
+__global__ void EigenConvolutionKernel3DGlobal(
+    InputEvaluator eval, const internal::IndexMapper<Index, InputDims, 3, InputEvaluator::Layout> indexMapper,
+    const float* kernel, Index total, Index numX, Index numY, Index numZ, Index kernelSizeX, Index kernelSizeY,
+    Index kernelSizeZ, float* buffer) {
+  const Index step = Index(blockDim.x) * gridDim.x;
+  for (Index output = Index(blockIdx.x) * blockDim.x + threadIdx.x; output < total;) {
+    Index remaining = output;
+    const Index x = remaining % numX;
+    remaining /= numX;
+    const Index y = remaining % numY;
+    remaining /= numY;
+    const Index z = remaining % numZ;
+    const Index plane = remaining / numZ;
+    const Index inputOffset = indexMapper.mapGpuInputPlaneToTensorInputOffset(plane);
+    float result = 0;
+    Index kernelIndex = 0;
+    for (Index k = 0; k < kernelSizeZ; ++k) {
+      for (Index j = 0; j < kernelSizeY; ++j) {
+        for (Index i = 0; i < kernelSizeX; ++i) {
+          result += eval.coeff(inputOffset + indexMapper.mapGpuInputKernelToTensorInputOffset(x + i, y + j, z + k)) *
+                    kernel[kernelIndex++];
+        }
+      }
+    }
+    buffer[indexMapper.mapGpuOutputPlaneToTensorOutputOffset(plane) +
+           indexMapper.mapGpuOutputKernelToTensorOutputOffset(x, y, z)] = result;
+    // Test the remaining distance before adding: even the final increment must not overflow Index.
+    if (step >= total - output) break;
+    output += step;
+  }
+}
+
 template <typename InputEvaluator, typename Index, typename InputDims>
 __global__ EIGEN_HIP_LAUNCH_BOUNDS_1024 void EigenConvolutionKernel3D(
     InputEvaluator eval, const internal::IndexMapper<Index, InputDims, 3, InputEvaluator::Layout> indexMapper,
@@ -1014,8 +1048,28 @@
         const int numX = dimensions()[m_indices[idxX]];
         const int numY = dimensions()[m_indices[idxY]];
         const int numZ = dimensions()[m_indices[idxZ]];
-        const int numP = dimensions().TotalSize() / (numX * numY * numZ);
+        const Index total = dimensions().TotalSize();
+        if (total == 0) return;
 
+        const array<Index, 3> indices{m_indices[idxX], m_indices[idxY], m_indices[idxZ]};
+        const array<Index, 3> kernel_dims{m_kernelImpl.dimensions()[idxX], m_kernelImpl.dimensions()[idxY],
+                                          m_kernelImpl.dimensions()[idxZ]};
+        internal::IndexMapper<Index, InputDims, 3, Layout> indexMapper(m_inputImpl.dimensions(), kernel_dims, indices);
+
+        if (m_kernelImpl.dimensions().TotalSize() > Index(maxSharedMem / sizeof(Scalar))) {
+          constexpr int preferredBlockSize = 256;
+          const int threads = numext::mini(preferredBlockSize, maxThreadsPerBlock);
+          const int maxBlocks = static_cast<int>(
+              numext::mini<int64_t>(int64_t(numMultiProcessors) * 32, NumTraits<Index>::highest() / threads));
+          const int blocks = static_cast<int>(numext::mini<Index>((total - 1) / threads + 1, Index(maxBlocks)));
+          LAUNCH_GPU_KERNEL(
+              (EigenConvolutionKernel3DGlobal<TensorEvaluator<InputArgType, GpuDevice>, Index, InputDims>), blocks,
+              threads, 0, m_device, m_inputImpl, indexMapper, m_kernel, total, Index(numX), Index(numY), Index(numZ),
+              Index(kernel_size_x), Index(kernel_size_y), Index(kernel_size_z), data);
+          break;
+        }
+
+        const int numP = total / (Index(numX) * numY * numZ);
         const int maxX = numext::mini<int>(
             128, numext::mini<int>(maxSharedMem / (sizeof(Scalar) * kernel_size_y * kernel_size_z) - kernel_size_x + 1,
                                    numX));
@@ -1039,11 +1093,6 @@
             (maxX + kernel_size_x - 1) * (maxY + kernel_size_y - 1) * (maxZ + kernel_size_z - 1) * sizeof(Scalar);
         gpu_assert(shared_mem <= maxSharedMem);
 
-        const array<Index, 3> indices{m_indices[idxX], m_indices[idxY], m_indices[idxZ]};
-        const array<Index, 3> kernel_dims{m_kernelImpl.dimensions()[idxX], m_kernelImpl.dimensions()[idxY],
-                                          m_kernelImpl.dimensions()[idxZ]};
-        internal::IndexMapper<Index, InputDims, 3, Layout> indexMapper(m_inputImpl.dimensions(), kernel_dims, indices);
-
         LAUNCH_GPU_KERNEL((EigenConvolutionKernel3D<TensorEvaluator<InputArgType, GpuDevice>, Index, InputDims>),
                           num_blocks, block_size, shared_mem, m_device, m_inputImpl, indexMapper, m_kernel, numP, numX,
                           maxX, numY, maxY, numZ, maxZ, kernel_size_x, kernel_size_y, kernel_size_z, data);
diff --git a/contrib/test/CMakeLists.txt b/contrib/test/CMakeLists.txt
index 3469f3a..c7ba51a 100644
--- a/contrib/test/CMakeLists.txt
+++ b/contrib/test/CMakeLists.txt
@@ -268,6 +268,7 @@
   ei_add_test(tensor_device)
   ei_add_test(tensor_gpu_device_attributes)
   ei_add_test(tensor_gpu)
+  ei_add_test(tensor_convolution_large_gpu)
   ei_add_test(tensor_gpu_runtime_check)
   ei_add_test(tensor_gpu_runtime_check_ndebug)
   ei_add_test(tensor_contract_gpu)
diff --git a/contrib/test/tensor_convolution_large_gpu.cu b/contrib/test/tensor_convolution_large_gpu.cu
new file mode 100644
index 0000000..a70d7ff
--- /dev/null
+++ b/contrib/test/tensor_convolution_large_gpu.cu
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: The Eigen Authors
+// SPDX-License-Identifier: MPL-2.0
+
+#define EIGEN_USE_GPU
+#define EIGEN_TEST_NO_LONGDOUBLE
+#define EIGEN_TEST_NO_COMPLEX
+#include "main.h"
+#include <contrib/Eigen/Tensor>
+
+template <int Layout, int NumDims, typename TensorIndex>
+void verify_gpu_convolution(const Tensor<float, NumDims, Layout, TensorIndex>& input,
+                            const Tensor<float, 3, Layout, TensorIndex>& kernel, const array<TensorIndex, 3>& axes,
+                            const Tensor<float, NumDims, Layout, TensorIndex>& expected) {
+  GpuStreamDevice stream;
+  GpuDevice device(&stream);
+  bool nonzero = false, varying = false;
+  for (Index i = 0; i < expected.size(); ++i) {
+    nonzero |= expected.data()[i] != 0;
+    varying |= expected.data()[i] != expected.data()[0];
+  }
+  VERIFY(nonzero);
+  VERIFY(varying);
+  Tensor<float, NumDims, Layout, TensorIndex> result;
+  result.resize(expected.dimensions());
+  float* a = static_cast<float*>(device.allocate(input.size() * sizeof(float)));
+  float* k = static_cast<float*>(device.allocate(kernel.size() * sizeof(float)));
+  float* out = static_cast<float*>(device.allocate(result.size() * sizeof(float)));
+  device.memcpyHostToDevice(a, input.data(), input.size() * sizeof(float));
+  device.memcpyHostToDevice(k, kernel.data(), kernel.size() * sizeof(float));
+  // NaN sentinels make missing output writes fail even when the reference is zero.
+  device.memset(out, 0xff, result.size() * sizeof(float));
+  TensorMap<Tensor<float, NumDims, Layout, TensorIndex>> gpu_input(a, input.dimensions());
+  TensorMap<Tensor<float, 3, Layout, TensorIndex>> gpu_kernel(k, kernel.dimensions());
+  TensorMap<Tensor<float, NumDims, Layout, TensorIndex>> gpu_result(out, result.dimensions());
+  gpu_result.device(device) = gpu_input.convolve(gpu_kernel, axes);
+  device.memcpyDeviceToHost(result.data(), out, result.size() * sizeof(float));
+  device.synchronize();
+  for (Index i = 0; i < result.size(); ++i) VERIFY_IS_EQUAL(result.data()[i], expected.data()[i]);
+  device.deallocate(a);
+  device.deallocate(k);
+  device.deallocate(out);
+}
+
+template <int Layout>
+void check_large_convolution(const array<Index, 3>& kernel_dims, const array<Index, 3>& axes) {
+  array<Index, 5> input_dims{{2, 3, 2, 3, 2}};
+  for (int i = 0; i < 3; ++i) input_dims[axes[i]] = kernel_dims[i] + i + 1;
+  Tensor<float, 5, Layout> input(input_dims);
+  Tensor<float, 3, Layout> kernel(kernel_dims);
+  // Small integers keep every product and partial sum exactly representable in float.
+  for (Index i = 0; i < input.size(); ++i) input.data()[i] = float((i * 17 + i / 7) % 19 - 9);
+  for (Index i = 0; i < kernel.size(); ++i) kernel.data()[i] = float((i * 7 + i / 5) % 7 - 2);
+  Tensor<float, 5, Layout> expected = input.convolve(kernel, axes);
+  verify_gpu_convolution(input, kernel, axes, expected);
+}
+
+template <int Layout, typename TensorIndex = Index>
+void check_convolution_grid_stride(Index output_size = 0) {
+  GpuStreamDevice stream;
+  GpuDevice device(&stream);
+  const Index kernel_size = device.sharedMemPerBlock() / sizeof(float) + 1;
+  const Index threads = numext::mini(256, device.maxGpuThreadsPerBlock());
+  const Index grid_stride = Index(device.getNumGpuMultiProcessors()) * 32 * threads;
+  if (output_size == 0) output_size = 2 * grid_stride + 17;
+  const Index max_input =
+      static_cast<Index>(numext::mini<int64_t>(NumTraits<Index>::highest(), NumTraits<TensorIndex>::highest()));
+  const Index max_output = max_input - kernel_size + 1;
+  // Some devices cannot fit an oversized convolution's input in this narrow index type.
+  if (max_output < 2) return;
+  output_size = numext::mini(output_size, max_output);
+  const array<TensorIndex, 3> axes{{0, 1, 2}};
+  Tensor<float, 3, Layout, TensorIndex> input(1, 1, TensorIndex(kernel_size + output_size - 1));
+  Tensor<float, 3, Layout, TensorIndex> kernel(1, 1, TensorIndex(kernel_size));
+  Tensor<float, 3, Layout, TensorIndex> expected(1, 1, TensorIndex(output_size));
+  input.setConstant(1);
+  input.data()[kernel_size + output_size / 2 - 1] = 2;
+  kernel.setConstant(1);
+  // An independent sliding sum avoids a CPU convolution of O(kernel_size * output_size).
+  Index sum = kernel_size;
+  for (Index i = 0; i < output_size; ++i) {
+    expected.data()[i] = float(sum);
+    if (i + 1 < output_size) sum += Index(input.data()[i + kernel_size]) - Index(input.data()[i]);
+  }
+  if (std::is_same<TensorIndex, Index>::value) VERIFY(output_size > grid_stride);
+  verify_gpu_convolution(input, kernel, axes, expected);
+}
+
+EIGEN_DECLARE_TEST(tensor_convolution_large_gpu) {
+  GpuStreamDevice stream;
+  GpuDevice device(&stream);
+  // Always exceed this device's shared-memory limit, including newer GPUs.
+  const Index oversized = device.sharedMemPerBlock() / (sizeof(float) * 3 * 50) + 1;
+  const Index capacity = device.sharedMemPerBlock() / sizeof(float);
+  const array<Index, 3> shapes[] = {{{3, 4, 2}},           {{3, 50, 120}},         {{3, 50, 145}},
+                                    {{3, 50, oversized}},  {{1, 1, capacity - 1}}, {{1, 1, capacity}},
+                                    {{1, 1, capacity + 1}}};
+  const array<Index, 3> axes[] = {{{1, 2, 3}}, {{0, 2, 4}}, {{4, 0, 2}}};
+  for (const auto& shape : shapes) {
+    for (const auto& axis : axes) {
+      CALL_SUBTEST(check_large_convolution<ColMajor>(shape, axis));
+      CALL_SUBTEST(check_large_convolution<RowMajor>(shape, axis));
+    }
+  }
+  CALL_SUBTEST(check_convolution_grid_stride<ColMajor>());
+  CALL_SUBTEST(check_convolution_grid_stride<RowMajor>());
+  CALL_SUBTEST((check_convolution_grid_stride<ColMajor, int16_t>(18000)));
+  CALL_SUBTEST((check_convolution_grid_stride<RowMajor, int16_t>(18000)));
+}