Skip to content

[DFT][cuFFT] Fix "Invalid strides" for shapes with a unit last dimension (#631) - #747

Open
zjin-lcf wants to merge 1 commit into
uxlfoundation:developfrom
zjin-lcf:fix/cufft-invalid-strides-unit-dim
Open

[DFT][cuFFT] Fix "Invalid strides" for shapes with a unit last dimension (#631)#747
zjin-lcf wants to merge 1 commit into
uxlfoundation:developfrom
zjin-lcf:fix/cufft-invalid-strides-unit-dim

Conversation

@zjin-lcf

@zjin-lcf zjin-lcf commented Jul 28, 2026

Copy link
Copy Markdown

Summary

Fixes #631.

oneapi::math::dft on the cuFFT backend throws dft/backends/cufft/commit: Invalid strides for multi-dimensional transforms whose last dimension has extent 1 (e.g. {3,1}, {2,1}, {5,1}, {2,3,1}). The same descriptors work on the Intel (MKL) backends, and the equivalent transform works when calling cuFFT directly.

Root cause

In src/dft/backends/cufft/commit.cpp, the backend uses std::min_element to find the dimension with the smallest stride (which becomes the cuFFT istride/ostride, i.e. the innermost dimension).

When a dimension has extent 1, its default stride ties with its neighbour's stride. std::min_element returns the first element among ties, so an outer dimension is mis-identified as the innermost one. This triggers the dimension-swap heuristic, producing a dimension/stride ordering that the check_stride_validity() pre-check then rejects — computing inner_n (e.g. 3) <= inner_stride (1) as false for both directions and throwing Invalid strides. The underlying cufftPlanMany call that would have been issued is actually valid.

There is also latent undefined behaviour: the swap logic computes the selected index from a_min after vec_a.erase(a_min) has invalidated that iterator. This is why the failure is compiler-dependent (it reproduces with icpx, but a recent open-source DPC++/clang build happens to take a different, passing path).

Fix

  • Search for the smallest stride in reverse, so ties resolve to the innermost (highest-index) dimension. This avoids the spurious dimension swap and lets valid transforms through.
  • Capture the selected stride index before erase(), removing the invalidated-iterator UB.

No behavioural change for shapes without unit dimensions: for strictly decreasing default strides the reverse search selects the same (last) element as before.

Test plan

Validated end-to-end on an NVIDIA sm_103 device, CUDA 13.2 / cuFFT 12.2, using an open-source DPC++ toolchain (intel/llvm nightly with the CUDA backend). Built oneMath with -DENABLE_CUFFT_BACKEND=ON -DTARGET_DOMAINS=dft.

With the fix, the reproducer from #631 and related shapes all pass:

shape {3,1}   : OK -> [(3,0) (0,0) (0,0)]
shape {2,1}   : OK -> [(2,0) (0,0)]
shape {5,1}   : OK -> [(5,0) (0,0) (0,0) (0,0) (0,0)]
shape {2,3,1} : OK -> [(6,0) ...]
shape {3,4}   : OK   (regression check)
shape {2,3,4} : OK   (regression check)
shape {8}     : OK   (regression check)
  • Confirmed direct cuFFT (cufftPlan2d(3,1) and the exact cufftPlanMany params oneMath generates) succeed on the B300 — the failure is in the pre-check, not cuFFT.
  • Standalone comparison of the stride algorithm: original logic REJECTS {3,1}, {2,1}, {5,1}, {2,3,1}; fixed logic ACCEPTS them; regression shapes unchanged.
  • End-to-end build + run on B300: all shapes above pass.
  • CI: DFT cuFFT functional tests.

Suggestion for maintainers: consider adding a DFT test case covering shapes with a unit last dimension to guard against regressions.

When a DFT descriptor has a dimension of extent 1 (e.g. {3,1}, {2,3,1}),
the default strides of that dimension tie with its neighbour's stride. The
cuFFT commit used std::min_element to locate the smallest-stride (innermost)
dimension, which returns the first tied element and thus mis-selects an outer
dimension as innermost. This triggered a spurious dimension swap and made the
check_stride_validity() pre-check reject a perfectly valid transform with
"Invalid strides", even though the underlying cufftPlanMany call succeeds.

Search for the smallest stride in reverse so ties resolve to the innermost
(highest-index) dimension, avoiding the incorrect swap. Also capture the
selected index before erase() to avoid using an invalidated iterator.

Fixes uxlfoundation#631.
@zjin-lcf
zjin-lcf requested a review from a team as a code owner July 28, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DFT][cuFFT] Invalid strides error on CUDA

1 participant