| // This file is part of Eigen, a lightweight C++ template library |
| // for linear algebra. |
| // |
| // Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com> |
| // |
| // This Source Code Form is subject to the terms of the Mozilla |
| // Public License v. 2.0. If a copy of the MPL was not distributed |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| // GPU FFT via cuFFT: 1D and 2D C2C, R2C, and C2R transforms with plan caching. |
| // |
| // The stream and cuBLAS handle come from a bound gpu::Context, which defaults to |
| // Context::threadLocal() so an FFT shares a stream with the thread's other GPU |
| // work; pass an explicit Context to bind elsewhere. |
| // |
| // Inverse transforms are scaled by 1/n (1D) or 1/(n*m) (2D), so inv(fwd(x)) == x |
| // as in Eigen's CPU FFT. |
| // |
| // Not thread-safe: concurrent fwd/inv calls on one instance race on the cached |
| // plans and the bound Context. Use one FFT per thread. |
| |
| #ifndef EIGEN_GPU_FFT_H |
| #define EIGEN_GPU_FFT_H |
| |
| // IWYU pragma: private |
| #include "./InternalHeaderCheck.h" |
| |
| #include "./CuFftSupport.h" |
| #include "./CuBlasSupport.h" |
| #include "./GpuContext.h" |
| |
| namespace Eigen { |
| namespace gpu { |
| // Default capacity of the per-FFT-instance cuFFT plan cache. Override by |
| // passing a capacity to the FFT constructor. |
| static constexpr std::size_t kDefaultCufftPlanCacheCapacity = 16; |
| |
| template <typename Scalar_> |
| class FFT { |
| public: |
| using Scalar = Scalar_; |
| using Complex = std::complex<Scalar>; |
| using ComplexVector = Matrix<Complex, Dynamic, 1>; |
| using RealVector = Matrix<Scalar, Dynamic, 1>; |
| using ComplexMatrix = Matrix<Complex, Dynamic, Dynamic, ColMajor>; |
| |
| /** Construct an FFT bound to the calling thread's default Context. |
| * The instance is thread-affine: it must not outlive the thread that |
| * constructed it, since it borrows a pointer into thread-local storage. |
| * For cross-thread lifetimes, pass an explicit Context. |
| * |
| * @param plan_cache_capacity max number of cuFFT plans to keep cached |
| * (silently clamped to at least 1). On overflow the least-recently-used |
| * plan is destroyed. */ |
| explicit FFT(std::size_t plan_cache_capacity = kDefaultCufftPlanCacheCapacity) |
| : ctx_(&Context::threadLocal()), plans_(plan_cache_capacity > 0 ? plan_cache_capacity : 1) {} |
| |
| /** Construct an FFT bound to the given Context. The Context must outlive |
| * this FFT instance; this object only borrows its stream and cuBLAS handle. |
| * |
| * @param plan_cache_capacity max number of cuFFT plans to keep cached |
| * (silently clamped to at least 1). On overflow the least-recently-used |
| * plan is destroyed. */ |
| explicit FFT(Context& ctx, std::size_t plan_cache_capacity = kDefaultCufftPlanCacheCapacity) |
| : ctx_(&ctx), plans_(plan_cache_capacity > 0 ? plan_cache_capacity : 1) {} |
| |
| // Destructor is implicit: ~LruCache destroys each CufftPlan, which calls |
| // cufftDestroy via CufftPlan's destructor. |
| |
| /** Capacity of the cuFFT plan cache (set at construction). */ |
| std::size_t plan_cache_capacity() const { return plans_.capacity(); } |
| |
| /** Current number of cached cuFFT plans. */ |
| std::size_t plan_cache_size() const { return plans_.size(); } |
| |
| FFT(const FFT&) = delete; |
| FFT& operator=(const FFT&) = delete; |
| |
| /** Forward 1D C2C FFT. */ |
| template <typename Derived, std::enable_if_t<NumTraits<typename Derived::Scalar>::IsComplex>* = nullptr> |
| ComplexVector fwd(const MatrixBase<Derived>& x) { |
| const ComplexVector input(x.derived()); |
| const int n = static_cast<int>(input.size()); |
| if (n == 0) return ComplexVector(0); |
| |
| ensure_buffers(n * sizeof(Complex), n * sizeof(Complex)); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(d_in_.get(), input.data(), n * sizeof(Complex), cudaMemcpyHostToDevice, ctx_->stream())); |
| |
| cufftHandle plan = get_plan_1d(n, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK(internal::cufftExecC2C_dispatch(plan, static_cast<Complex*>(d_in_.get()), |
| static_cast<Complex*>(d_out_.get()), CUFFT_FORWARD)); |
| |
| ComplexVector result(n); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(result.data(), d_out_.get(), n * sizeof(Complex), cudaMemcpyDeviceToHost, ctx_->stream())); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| return result; |
| } |
| |
| /** Inverse 1D C2C FFT. Scaled by 1/n. */ |
| template <typename Derived> |
| ComplexVector inv(const MatrixBase<Derived>& X) { |
| static_assert(NumTraits<typename Derived::Scalar>::IsComplex, "inv() requires complex input"); |
| const ComplexVector input(X.derived()); |
| const int n = static_cast<int>(input.size()); |
| if (n == 0) return ComplexVector(0); |
| |
| ensure_buffers(n * sizeof(Complex), n * sizeof(Complex)); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(d_in_.get(), input.data(), n * sizeof(Complex), cudaMemcpyHostToDevice, ctx_->stream())); |
| |
| cufftHandle plan = get_plan_1d(n, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK(internal::cufftExecC2C_dispatch(plan, static_cast<Complex*>(d_in_.get()), |
| static_cast<Complex*>(d_out_.get()), CUFFT_INVERSE)); |
| |
| // Scale by 1/n. |
| EIGEN_CUBLAS_CHECK( |
| internal::cublasXscal(ctx_->cublasHandle(), n, Scalar(1) / Scalar(n), static_cast<Complex*>(d_out_.get()), 1)); |
| |
| ComplexVector result(n); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(result.data(), d_out_.get(), n * sizeof(Complex), cudaMemcpyDeviceToHost, ctx_->stream())); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| return result; |
| } |
| |
| /** Forward 1D R2C FFT. Returns n/2+1 complex values (half-spectrum). */ |
| template <typename Derived, std::enable_if_t<!NumTraits<typename Derived::Scalar>::IsComplex>* = nullptr> |
| ComplexVector fwd(const MatrixBase<Derived>& x) { |
| const RealVector input(x.derived()); |
| const int n = static_cast<int>(input.size()); |
| if (n == 0) return ComplexVector(0); |
| |
| const int n_complex = n / 2 + 1; |
| ensure_buffers(n * sizeof(Scalar), n_complex * sizeof(Complex)); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(d_in_.get(), input.data(), n * sizeof(Scalar), cudaMemcpyHostToDevice, ctx_->stream())); |
| |
| cufftHandle plan = get_plan_1d(n, internal::cufft_r2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK( |
| internal::cufftExecR2C_dispatch(plan, static_cast<Scalar*>(d_in_.get()), static_cast<Complex*>(d_out_.get()))); |
| |
| ComplexVector result(n_complex); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(result.data(), d_out_.get(), n_complex * sizeof(Complex), |
| cudaMemcpyDeviceToHost, ctx_->stream())); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| return result; |
| } |
| |
| /** Inverse 1D C2R FFT. Input is n/2+1 complex values, output is nfft real values. |
| * Scaled by 1/nfft. Caller must specify nfft (original real signal length). */ |
| template <typename Derived> |
| RealVector invReal(const MatrixBase<Derived>& X, Index nfft) { |
| static_assert(NumTraits<typename Derived::Scalar>::IsComplex, "invReal() requires complex input"); |
| const ComplexVector input(X.derived()); |
| const int n = static_cast<int>(nfft); |
| const int n_complex = n / 2 + 1; |
| eigen_assert(input.size() == n_complex); |
| if (n == 0) return RealVector(0); |
| |
| ensure_buffers(n_complex * sizeof(Complex), n * sizeof(Scalar)); |
| // cuFFT C2R may overwrite the input, so we copy to d_in_. |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_in_.get(), input.data(), n_complex * sizeof(Complex), |
| cudaMemcpyHostToDevice, ctx_->stream())); |
| |
| cufftHandle plan = get_plan_1d(n, internal::cufft_c2r_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK( |
| internal::cufftExecC2R_dispatch(plan, static_cast<Complex*>(d_in_.get()), static_cast<Scalar*>(d_out_.get()))); |
| |
| // Scale by 1/n. |
| EIGEN_CUBLAS_CHECK( |
| internal::cublasXscal(ctx_->cublasHandle(), n, Scalar(1) / Scalar(n), static_cast<Scalar*>(d_out_.get()), 1)); |
| |
| RealVector result(n); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(result.data(), d_out_.get(), n * sizeof(Scalar), cudaMemcpyDeviceToHost, ctx_->stream())); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| return result; |
| } |
| |
| /** Forward 2D C2C FFT. Input and output are rows x cols complex matrices. */ |
| template <typename Derived> |
| ComplexMatrix fwd2(const MatrixBase<Derived>& A) { |
| static_assert(NumTraits<typename Derived::Scalar>::IsComplex, "fwd2() requires complex input"); |
| const ComplexMatrix input(A.derived()); |
| const int rows = static_cast<int>(input.rows()); |
| const int cols = static_cast<int>(input.cols()); |
| if (rows == 0 || cols == 0) return ComplexMatrix(rows, cols); |
| |
| const size_t total = static_cast<size_t>(rows) * static_cast<size_t>(cols) * sizeof(Complex); |
| ensure_buffers(total, total); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_in_.get(), input.data(), total, cudaMemcpyHostToDevice, ctx_->stream())); |
| |
| cufftHandle plan = get_plan_2d(rows, cols, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK(internal::cufftExecC2C_dispatch(plan, static_cast<Complex*>(d_in_.get()), |
| static_cast<Complex*>(d_out_.get()), CUFFT_FORWARD)); |
| |
| ComplexMatrix result(rows, cols); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(result.data(), d_out_.get(), total, cudaMemcpyDeviceToHost, ctx_->stream())); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| return result; |
| } |
| |
| /** Inverse 2D C2C FFT. Scaled by 1/(rows*cols). */ |
| template <typename Derived> |
| ComplexMatrix inv2(const MatrixBase<Derived>& A) { |
| static_assert(NumTraits<typename Derived::Scalar>::IsComplex, "inv2() requires complex input"); |
| const ComplexMatrix input(A.derived()); |
| const int rows = static_cast<int>(input.rows()); |
| const int cols = static_cast<int>(input.cols()); |
| if (rows == 0 || cols == 0) return ComplexMatrix(rows, cols); |
| |
| const size_t total = static_cast<size_t>(rows) * static_cast<size_t>(cols) * sizeof(Complex); |
| ensure_buffers(total, total); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_in_.get(), input.data(), total, cudaMemcpyHostToDevice, ctx_->stream())); |
| |
| cufftHandle plan = get_plan_2d(rows, cols, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK(internal::cufftExecC2C_dispatch(plan, static_cast<Complex*>(d_in_.get()), |
| static_cast<Complex*>(d_out_.get()), CUFFT_INVERSE)); |
| |
| // Scale by 1/(rows*cols). |
| const int total_elems = rows * cols; |
| EIGEN_CUBLAS_CHECK(internal::cublasXscal(ctx_->cublasHandle(), total_elems, Scalar(1) / Scalar(total_elems), |
| static_cast<Complex*>(d_out_.get()), 1)); |
| |
| ComplexMatrix result(rows, cols); |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(result.data(), d_out_.get(), total, cudaMemcpyDeviceToHost, ctx_->stream())); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| return result; |
| } |
| |
| // DeviceMatrix in / DeviceMatrix out: no host transfer and no host |
| // synchronization. Cross-stream safety follows the DeviceMatrix event |
| // protocol (waitReady before reading, recordReady after enqueuing). 1D |
| // overloads expect column vectors. |
| |
| /** Forward 1D C2C FFT, device-resident: d_X = fft(d_x). */ |
| void fwd(const DeviceMatrix<Complex>& d_x, DeviceMatrix<Complex>& d_X) { |
| eigen_assert(d_x.cols() <= 1 && "device fwd(): expected a column vector"); |
| const int n = internal::to_blas_int(d_x.rows()); |
| prepare_out(d_x, d_X, n, 1); |
| if (n == 0) return; |
| cufftHandle plan = get_plan_1d(n, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK( |
| internal::cufftExecC2C_dispatch(plan, const_cast<Complex*>(d_x.data()), d_X.data(), CUFFT_FORWARD)); |
| d_X.recordReady(ctx_->stream()); |
| } |
| |
| /** Inverse 1D C2C FFT, device-resident. Scaled by 1/n. */ |
| void inv(const DeviceMatrix<Complex>& d_X, DeviceMatrix<Complex>& d_x) { |
| eigen_assert(d_X.cols() <= 1 && "device inv(): expected a column vector"); |
| const int n = internal::to_blas_int(d_X.rows()); |
| prepare_out(d_X, d_x, n, 1); |
| if (n == 0) return; |
| cufftHandle plan = get_plan_1d(n, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK( |
| internal::cufftExecC2C_dispatch(plan, const_cast<Complex*>(d_X.data()), d_x.data(), CUFFT_INVERSE)); |
| EIGEN_CUBLAS_CHECK(internal::cublasXscal(ctx_->cublasHandle(), n, Scalar(1) / Scalar(n), d_x.data(), 1)); |
| d_x.recordReady(ctx_->stream()); |
| } |
| |
| /** Forward 1D R2C FFT, device-resident. Output is n/2+1 complex values. */ |
| void fwd(const DeviceMatrix<Scalar>& d_x, DeviceMatrix<Complex>& d_X) { |
| eigen_assert(d_x.cols() <= 1 && "device fwd(): expected a column vector"); |
| const int n = internal::to_blas_int(d_x.rows()); |
| const int n_complex = n / 2 + 1; |
| prepare_out(d_x, d_X, n == 0 ? 0 : n_complex, 1); |
| if (n == 0) return; |
| cufftHandle plan = get_plan_1d(n, internal::cufft_r2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK(internal::cufftExecR2C_dispatch(plan, const_cast<Scalar*>(d_x.data()), d_X.data())); |
| d_X.recordReady(ctx_->stream()); |
| } |
| |
| /** Inverse 1D C2R FFT, device-resident. Input is nfft/2+1 complex values, |
| * output nfft real values, scaled by 1/nfft. The input is preserved (cuFFT |
| * C2R may overwrite its input, so it is staged through internal scratch). */ |
| void invReal(const DeviceMatrix<Complex>& d_X, DeviceMatrix<Scalar>& d_x, Index nfft) { |
| eigen_assert(d_X.cols() <= 1 && "device invReal(): expected a column vector"); |
| const int n = internal::to_blas_int(nfft); |
| const int n_complex = n / 2 + 1; |
| eigen_assert(n == 0 || d_X.rows() == n_complex); |
| prepare_out(d_X, d_x, n, 1); |
| if (n == 0) return; |
| // Stage through d_in_: cuFFT C2R may overwrite the input array. |
| ensure_buffers(static_cast<size_t>(n_complex) * sizeof(Complex), 0); |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(d_in_.get(), d_X.data(), n_complex * sizeof(Complex), |
| cudaMemcpyDeviceToDevice, ctx_->stream())); |
| cufftHandle plan = get_plan_1d(n, internal::cufft_c2r_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK(internal::cufftExecC2R_dispatch(plan, static_cast<Complex*>(d_in_.get()), d_x.data())); |
| EIGEN_CUBLAS_CHECK(internal::cublasXscal(ctx_->cublasHandle(), n, Scalar(1) / Scalar(n), d_x.data(), 1)); |
| d_x.recordReady(ctx_->stream()); |
| } |
| |
| /** Forward 2D C2C FFT, device-resident. */ |
| void fwd2(const DeviceMatrix<Complex>& d_A, DeviceMatrix<Complex>& d_B) { |
| const int rows = internal::to_blas_int(d_A.rows()); |
| const int cols = internal::to_blas_int(d_A.cols()); |
| prepare_out(d_A, d_B, rows, cols); |
| if (rows == 0 || cols == 0) return; |
| cufftHandle plan = get_plan_2d(rows, cols, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK( |
| internal::cufftExecC2C_dispatch(plan, const_cast<Complex*>(d_A.data()), d_B.data(), CUFFT_FORWARD)); |
| d_B.recordReady(ctx_->stream()); |
| } |
| |
| /** Inverse 2D C2C FFT, device-resident. Scaled by 1/(rows*cols). */ |
| void inv2(const DeviceMatrix<Complex>& d_A, DeviceMatrix<Complex>& d_B) { |
| const int rows = internal::to_blas_int(d_A.rows()); |
| const int cols = internal::to_blas_int(d_A.cols()); |
| prepare_out(d_A, d_B, rows, cols); |
| if (rows == 0 || cols == 0) return; |
| cufftHandle plan = get_plan_2d(rows, cols, internal::cufft_c2c_type<Scalar>::value); |
| EIGEN_CUFFT_CHECK( |
| internal::cufftExecC2C_dispatch(plan, const_cast<Complex*>(d_A.data()), d_B.data(), CUFFT_INVERSE)); |
| const int total_elems = internal::to_blas_int(static_cast<int64_t>(rows) * static_cast<int64_t>(cols)); |
| EIGEN_CUBLAS_CHECK( |
| internal::cublasXscal(ctx_->cublasHandle(), total_elems, Scalar(1) / Scalar(total_elems), d_B.data(), 1)); |
| d_B.recordReady(ctx_->stream()); |
| } |
| |
| /** The CUDA stream borrowed from the bound Context. */ |
| cudaStream_t stream() const { return ctx_->stream(); } |
| |
| /** The Context this FFT is bound to. */ |
| Context& context() const { return *ctx_; } |
| |
| private: |
| Context* ctx_; |
| Eigen::internal::LruCache<int64_t, internal::CufftPlan> plans_; |
| internal::DeviceBuffer d_in_; |
| internal::DeviceBuffer d_out_; |
| size_t d_in_size_ = 0; |
| size_t d_out_size_ = 0; |
| |
| // Common device-transform prologue: alias check, input/output event waits, |
| // and (destructive) output resize. |
| template <typename InScalar, typename OutScalar> |
| void prepare_out(const DeviceMatrix<InScalar>& in, DeviceMatrix<OutScalar>& out, Index out_rows, Index out_cols) { |
| eigen_assert( |
| (in.data() == nullptr || static_cast<const void*>(in.data()) != static_cast<const void*>(out.data())) && |
| "device FFT: output must not alias input"); |
| in.waitReady(ctx_->stream()); |
| if (!out.empty()) out.waitReady(ctx_->stream()); |
| out.resize(out_rows, out_cols); |
| } |
| |
| // Buffers grow but never shrink. The pre-realloc sync drains the *bound* |
| // Context's stream — including unrelated GEMMs/solves/`device(ctx) = ...` |
| // assignments queued on it — so callers running FFTs alongside other GPU |
| // work on the same Context should size up front (call fwd/inv with the |
| // largest expected n once) to avoid mid-pipeline stalls. |
| void ensure_buffers(size_t in_bytes, size_t out_bytes) { |
| if (in_bytes > d_in_size_) { |
| if (d_in_) EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| d_in_ = internal::DeviceBuffer(in_bytes); |
| d_in_size_ = in_bytes; |
| } |
| if (out_bytes > d_out_size_) { |
| if (d_out_) EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(ctx_->stream())); |
| d_out_ = internal::DeviceBuffer(out_bytes); |
| d_out_size_ = out_bytes; |
| } |
| } |
| |
| // Plan key encoding: rank (1 bit) | type (4 bits) | dims. |
| // cufftType uses 7 bits; the top 3 (precision discriminator) are redundant |
| // since Scalar fixes precision per FFT instance, so mask to 4 bits — without |
| // it, e.g. plan_key_1d(5, C2C) and plan_key_1d(7, C2C) collide. |
| static constexpr int64_t kTypeMask = 0xF; |
| static constexpr int kCols2DBits = 30; // bits 5..34 |
| static constexpr int kRows2DBits = 29; // bits 35..63 |
| static int64_t plan_key_1d(int n, cufftType type) { return (int64_t(n) << 5) | (int64_t(type & kTypeMask) << 1) | 0; } |
| |
| static int64_t plan_key_2d(int rows, int cols, cufftType type) { |
| eigen_assert(rows >= 0 && int64_t(rows) < (int64_t(1) << kRows2DBits) && |
| "FFT plan rows exceed plan-key bit budget"); |
| eigen_assert(cols >= 0 && int64_t(cols) < (int64_t(1) << kCols2DBits) && |
| "FFT plan cols exceed plan-key bit budget"); |
| return (int64_t(rows) << 35) | (int64_t(cols) << 5) | (int64_t(type & kTypeMask) << 1) | 1; |
| } |
| |
| cufftHandle get_plan_1d(int n, cufftType type) { |
| const int64_t key = plan_key_1d(n, type); |
| if (internal::CufftPlan* hit = plans_.find(key)) return hit->get(); |
| |
| cufftHandle plan; |
| EIGEN_CUFFT_CHECK(cufftPlan1d(&plan, n, type, /*batch=*/1)); |
| EIGEN_CUFFT_CHECK(cufftSetStream(plan, ctx_->stream())); |
| return plans_.insert(key, internal::CufftPlan(plan))->get(); |
| } |
| |
| cufftHandle get_plan_2d(int rows, int cols, cufftType type) { |
| const int64_t key = plan_key_2d(rows, cols, type); |
| if (internal::CufftPlan* hit = plans_.find(key)) return hit->get(); |
| |
| // cuFFT uses row-major (C order) for 2D: first dim = rows, second = cols. |
| // Eigen matrices are column-major, so we pass (cols, rows) to cuFFT |
| // to get the correct 2D transform. |
| cufftHandle plan; |
| EIGEN_CUFFT_CHECK(cufftPlan2d(&plan, cols, rows, type)); |
| EIGEN_CUFFT_CHECK(cufftSetStream(plan, ctx_->stream())); |
| return plans_.insert(key, internal::CufftPlan(plan))->get(); |
| } |
| }; |
| } // namespace gpu |
| } // namespace Eigen |
| |
| #endif // EIGEN_GPU_FFT_H |