Skip to content

Add the capability to do adjoint transforms #633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open

Conversation

mreineck
Copy link
Collaborator

This is a first outline how I propose to add adjoint transforms; it is mainly meant as a basis for discussions and measurements. The computation of the actual adjoint is completely untested yet, but the standard functionality should still be OK, as far as my tests show.

@ahbarnett, @DiamonDinoia please let me know your thoughts on this one!

@mreineck
Copy link
Collaborator Author

Just to be clear: at this point, there is no interface support for the new functioality. I just want to show which kind of impact the new feature has on the existing implementation.

@mreineck
Copy link
Collaborator Author

I've added the C and Python interfaces, as well as basic Python unit tests.

@mreineck
Copy link
Collaborator Author

Test failures seem to be "near misses" in adjoint type 3 transforms. No idea why that direction should be less accurate, and why it only happens in some of the tests.
This needs more investigation, but the approach seems to work well in principle.

@mreineck
Copy link
Collaborator Author

mreineck commented Feb 19, 2025

Implements #571 and #566 (on CPU).

…s. Perhaps 1e-6 is just a bit too close to the machine epsilon for single precision
@mreineck
Copy link
Collaborator Author

I think this is ready for "technical" review. If there is agreement that the change is desirable, I can try to

  • provide more language-specific interfaces
  • document the new function

Copy link
Collaborator Author

@mreineck mreineck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few explanatory comments

@@ -20,7 +20,7 @@
strt = time.time()

#plan
plan = fp.Plan(1,(N,),dtype='single')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated to the PR, but without it, CI simply fails. I don't know why this hasn't caused issues so far.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems because we removed the conversion for real dtypes in https://github.com/flatironinstitute/finufft/pull/606/files

@@ -34,7 +34,7 @@

# instantiate the plan (note n_trans must be set here), also setting tolerance:
t0 = time.time()
plan = finufft.Plan(nufft_type, (N1, N2), eps=1e-4, n_trans=K, dtype='float32')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated to the PR, but without it, CI simply fails. I don't know why this hasn't caused issues so far.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this, it seems all the *f.py examples(in python/finufft/examples) should change according to PR606? otherwise the check is_single_dtype(dtype)

is_single = is_single_dtype(dtype)
will fail.

@@ -86,6 +120,19 @@ def test_finufft3_plan(dtype, dim, n_source_pts, n_target_pts, output_arg):

utils.verify_type3(source_pts, source_coefs, target_pts, target_coefs, 1e-6)

# test adjoint type 3
plan = Plan(3, dim, dtype=dtype, isign=-1, eps=1e-5)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm increasing eps from 1e-6 to 1e-5 here, because I get occasional failures with single precision otherwise. Given that 1e-6 is uncomfortably close to machine epsilon, I'm not too worried about this change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. type 3 errors are usually 2-3x bigger than type 1 or 2 at the same tolerance.

@@ -154,7 +154,7 @@ def verify_type1(pts, coefs, shape, sig_est, tol):

type1_rel_err = np.linalg.norm(fk_target - fk_est) / np.linalg.norm(fk_target)

assert type1_rel_err < 25 * tol
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching from assert to np.testing.assert_allclose here, because the latter will provide more information in case of failure, which speeds up debugging a lot.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea

}
}
}
#endif
#else
p->fftPlan->execute(); // if thisBatchSize<batchSize it wastes some flops
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs discussion: how do we want to deal with this situation? The trick used here is nice and simple, but it will make the adjoint call slower than the forward one.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't understand the trick or why it's slower - sorry for my stupidity. I'd also like to understand how FFTW is handled. Since your change to the fft.h adds the data pointer, can that work with FFTW too (and allow us to allocate-at-execute stage) ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trick is the equivalence:
fft_adjoint(x) = conj(fft(conj(x)))
(since we do not have a plan for fft_adjoint, we just mess around with the sign of the imaginary part of input and output to get te same effect as if we had switched isign).
The additional complex conjugate of input and output is of course not completely free, but it might be sub-dominant compared to the cost of the FFT itself.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yes, we do allocate the temporary at the execute stage in this PR, also for FFTW.

@ahbarnett
Copy link
Collaborator

ahbarnett commented Feb 25, 2025 via email

Copy link
Collaborator

@ahbarnett ahbarnett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Martin,

[This is supposed to go as a PR review but appeared as a comment]

Your work here is great. As usual you are able to slot something in with minimal disruption. It will certainly allow power users to slip in the adjoint when needed. I don't understand how the FFTW part works - there seem to be no changes which would allow the fftw_execute to change type (or handle the new data ptr in the fft.h interface) - or did I miss it?

However, I am thinking about the interface from the new user perspective, and think it is getting more confusing than needs be. In your PR a user plans a type 1, then can do its adjoint (type 2 with flipped isign) freely. Users will want to know if that's the same a planning a type 2 and doing a forward - it's not due to isign. (Extra confusion: in Europe t1 is called "adjoint" and t2 "forward" NUDFT.) I see the isign flip between going from 1 to 2 vs 1 to its adj being a major source of confusion. The motivation for your PR was users wanting a single plan for t1 and t2, or for t1 and its adj. I think we should bounce around ideas for a new interface that fits the need. An idea to reduce confusion is to have a "plan 1 and 2", and then execute_1(c,f,isign) execute_2(c,f,isign). No confusion. 1+ is adj of 2-, and 1- adj of 2+. Question is can we do it in a neat way that preserves the existing guru interface (add a type=12 to the plan?) since legacy users need to still be able to plan a type 2 explicitly and have plan execute(c,f) do that t2.
Other questions are: 4 if FFTW plan can handle the isign swtiching?
This would all be for the plan (guru) interface. It would expand that interface from 4 to six commands. Under the hood (as you do in this PR with execute_internal()), execute() could call execute_1 or _2.

I have lots of deadlines in the next month so will have to wait a bit, but I'd love to discuss this and hash out the best interface, since I don't want a confusing interface to become locked in...

Best, Alex

}
}
}
#endif
#else
p->fftPlan->execute(); // if thisBatchSize<batchSize it wastes some flops
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't understand the trick or why it's slower - sorry for my stupidity. I'd also like to understand how FFTW is handled. Since your change to the fft.h adds the data pointer, can that work with FFTW too (and allow us to allocate-at-execute stage) ?

@@ -424,72 +425,76 @@ static void deconvolveshuffle3d(int dir, T prefac, std::vector<T> &ker1,
// --------- batch helper functions for t1,2 exec: ---------------------------

template<typename T>
static int spreadinterpSortedBatch(int batchSize, FINUFFT_PLAN_T<T> *p,
std::complex<T> *fwBatch, std::complex<T> *cBatch)
static int spreadinterpSortedBatch(int batchSize, const FINUFFT_PLAN_T<T> &p,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THis is just part of your ongoing cleanup, right?

Copy link
Collaborator Author

@mreineck mreineck Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly yes. But the change to const also ensures that we don't do things like

  innerT2plan->ntrans = thisBatchSize;

any more (which we had in execute() before). This sort of "messing with the state of another object" can cause a lot of trouble once plans are perhaps invoked in parallel ... and that's what I wanted to make possible with this patch.
The switch from pointer to reference is cosmetics, and I should perhaps have delayed that until later ... sorry,

/* See ../docs/cguru.doc for current documentation.

For given (stack of) weights cj or coefficients fk, performs NUFFTs with
existing (sorted) NU pts and existing plan.
For type 1 and 3: cj is input, fk is output.
For type 2: fk is input, cj is output.
For adjoint == false:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these comments are super helpful

- 0 < ntrans_actual <= batchSize: instead of doing ntrans transforms,
perform only ntrans_actual

scratch_size, aligned_scratch:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we should do anyway with t3? (regardless of adjoint?)

@mreineck
Copy link
Collaborator Author

I just added two lines to the Changelog mentioning the thread safety and allocate-on-execute feature.

From my side this is ready to merge (assuming that the missing interfaces/docs/tests can be provided in a separate PR).

@mreineck mreineck marked this pull request as ready for review June 26, 2025 06:23
@DiamonDinoia
Copy link
Collaborator

I just added two lines to the Changelog mentioning the thread safety and allocate-on-execute feature.

From my side this is ready to merge (assuming that the missing interfaces/docs/tests can be provided in a separate PR).

I think only fortran is missing?

For GPU, It will take some time be implement this. I plan to understand everything while reviewing and integrating it once I have time.

@mreineck
Copy link
Collaborator Author

I think only fortran is missing?

Interface-wise, yes. Comprehensive tests still need to be done (together with the standard guru interface tests).
Actually, the necessary changes for the Fortran interface should be minimal, I can probably add them quickly. But without testing they shouldn't be trusted.

@ahbarnett
Copy link
Collaborator

uh-oh, I just did the same thing!

@ahbarnett
Copy link
Collaborator

Hang on and let me merge too. I did the complementary example. I changed finufftfort.cpp which I see you didn't yet..

@mreineck
Copy link
Collaborator Author

Oops, I actually did, but I forgot the git add ...
Please go ahead!

@ahbarnett
Copy link
Collaborator

Ok, merged and pushed - diff finufftfort.cpp and see it we are bitwise identical :)

@ahbarnett
Copy link
Collaborator

I see your rel err is 0.44E-02 when I run your guru1d2_adjoint{f} ...

@ahbarnett
Copy link
Collaborator

You forgot to negate isign :)

@ahbarnett
Copy link
Collaborator

I'll fix and clean up the headers for those files. I can also finish up the fortran docs.

For the guru tests, one idea is to have a dedicated test of adjointness that tests <f,A^g> = <Af,g> where f,g are random vectors, and A^ means execute_adjoint, A means execute. THis would be somewhat easier to write than a new "full math test of guru" C++ code.

@mreineck
Copy link
Collaborator Author

You forgot to negate isign :)

True ;-)

We now have a file called guru1d1_adjoint.f, which is identical to guru1d2_adjoint.f. Should we remove it?

@ahbarnett
Copy link
Collaborator

Have a look at guru1d1_adjoint.f - the math is different, and it's basically a new type-2 tester (with iflag flipped). I'd like to keep it. But your ones need commenting for users, and maybe I'll simplify them to a single guru call...

@ahbarnett
Copy link
Collaborator

ok, fortran done now :)

@mreineck
Copy link
Collaborator Author

Beautiful, thanks!

@DiamonDinoia
Copy link
Collaborator

@mreineck, @ahbarnett is it ready for a thorough review?

@mreineck
Copy link
Collaborator Author

I'd say yes.

@ahbarnett
Copy link
Collaborator

ahbarnett commented Jun 27, 2025 via email

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.

allow type 1 and 2 plans to be interchangeable to reduce from two to one plan for transform pairs
4 participants