Skip to content
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

Avoid calling wcsset if the wcsprm is unchanged #16411

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

astrofrog
Copy link
Member

@astrofrog astrofrog commented May 7, 2024

This pull request is an attempt to fix #16245 - it is just a proof of concept at this point and should not be merged yet, we first need to agree whether this is a reasonable workaround. The issues in #16245 is caused fundamentally by wcsset not being thread-safe and being called from different threads. However, these calls are not necessary because the WCS is not changing between all the calls. So the approach in this PR is to keep track of what the wcsprm was after the last time wcsset was called, and exit early if it hasn't changed since.

Note that using flag as set by note_change is not sufficient, because users can modify some of the array attributes of the WCS in-place, such as:

wcs.wcs.cdelt[1] = 1

which won't trigger note_change.

As also noted in #16409, using wcsprm_python2c and wcsprm_c2python itself is not really thread-safe though that can be addressed separately as it is used in other places too and luckily doesn't seem to actually trigger threading issues in practice. With this PR, the example in #16245 outputs:

Mismatching elements: 0
Mismatching elements: 0
Mismatching elements: 0
Mismatching elements: 0
Mismatching elements: 0
Mismatching elements: 0
Mismatching elements: 0
Mismatching elements: 0

I need to assess what the performance impact of this PR is.

cc @manodeep

Copy link

github-actions bot commented May 7, 2024

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

Copy link

github-actions bot commented May 7, 2024

👋 Thank you for your draft pull request! Do you know that you can use [ci skip] or [skip ci] in your commit messages to skip running continuous integration tests until you are ready?

@pllim pllim added this to the v7.0.0 milestone May 7, 2024
@astrofrog
Copy link
Member Author

Just to put it on the table, an alternative is to compute a checksum of the wcsprm, but that would be more work as there are a lot of attributes on wcsprm.

// using x_prev, and we compare these. If equal, and if PyWcsprm_cset() succeeded
// last time it was called, then we can just return 0 as there should be nothing
// to do.
if(self->x_prev_set) {
Copy link
Contributor

@manodeep manodeep May 7, 2024

Choose a reason for hiding this comment

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

self->x_prev_set might need to be initialised somewhere in the init (perhaps where x.flag is initialised) otherwise this could potentially be a use of uninitialised memory

wcsprm_c2python(&self->x);
wcsprm_c2python(&self->x_prev);
if (status == 0 && equal != 0) return 0;
}

if (convert) wcsprm_python2c(&self->x);
status = wcsset(&self->x);
if (convert) wcsprm_c2python(&self->x);

if (status == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor issue: From what I can tell, wcsset is called and returns successfully even when given incomplete details, i.e., with naxis = 0. May be we can add the status == 0 && naxis > 0 for only storing a fully initialised wcsprm struct.

@manodeep
Copy link
Contributor

manodeep commented May 7, 2024

[Adding what Tom and I discussed over slack] This fix is a trade-off in ensuring that wcsset is only called when necessary - the trade-off being that wcsset will be called even when a changed field does not require a wcsset call. This fix also ensures that the solution continues to work in the future when a new field might be introduced where changed values require a wcsset call.

(The correct solution would require modifying all relevant getters (where the list of relevant getters might change in the future) to store the present values within a malloc'ed buffer, the number of data bytes, and another address that contains the memory address the getter refers to. Any call to wcsset would then have to compare the previously stored values with the current contents, potentially as a memcmp assuming no padding bytes are involved (i.e., structs containing padding bytes between fields) are involved. Such a correct fix would be more complex and more invasive than Tom's fix)

@manodeep
Copy link
Contributor

manodeep commented May 8, 2024

Here are the relevant fields that require a wcsset when changed (taken from these lines)

*       - wcsprm::naxis (q.v., not normally set by the user),
*       - wcsprm::crpix,
*       - wcsprm::pc,
*       - wcsprm::cdelt,
*       - wcsprm::crval,
*       - wcsprm::cunit,
*       - wcsprm::ctype,
*       - wcsprm::lonpole,
*       - wcsprm::latpole,
*       - wcsprm::restfrq,
*       - wcsprm::restwav,
*       - wcsprm::npv,
*       - wcsprm::pv,
*       - wcsprm::nps,
*       - wcsprm::ps,
*       - wcsprm::cd,
*       - wcsprm::crota,
*       - wcsprm::altlin,
*       - wcsprm::ntab,
*       - wcsprm::nwtb,
*       - wcsprm::tab,
*       - wcsprm::wtb.

@astrofrog astrofrog mentioned this pull request May 14, 2024
1 task
@manodeep
Copy link
Contributor

With wcslib v8.3, there is a new function int wcsenq(const struct wcsprm *wcs, int enquiry) which when invoked as wcsenq(wcs, WCSENQ_CHK) will return 0 when the previous and current checksums don't match signalling that wcsset() needs to be called again. This can (hopefully) detect cases where some values of an array/list parameter (i.e., where the getters are invoked, rather than the setters) have been changed and that wcsset() needs to be called again.
`

@astrofrog astrofrog force-pushed the avoid-unecessary-wcsset branch 2 times, most recently from 50a15cc to fc69c4a Compare May 14, 2024 13:24
@astrofrog
Copy link
Member Author

astrofrog commented May 14, 2024

@manodeep - I've now rebased on top of the WCSLIB 8.3 PR, and have then made it so we check the checksum prior to calling wcsset. The last commit is, I think a necessary bug fix in WCSLIB, which I've reported to Mark.

With this PR, I don't see the multi-threading issues anymore! (well the ones related to wcsset)

@astrofrog astrofrog added Bug backport-v6.1.x on-merge: backport to v6.1.x labels May 14, 2024
@astrofrog astrofrog requested a review from manodeep May 15, 2024 09:37
@astrofrog
Copy link
Member Author

The WCSLIB bug fixes were approved by Mark - he said he will not release 8.3.1 yet but we can always hold off from merging this until it is if people prefer not to patch WCSLIB ourselves in the meantime.

@manodeep - in the meantime, could you review this and let me know what you think?

@astrofrog
Copy link
Member Author

This is not easy to properly test in our test suite - it would need to rely on a multi-threading test and the behavior is non-deterministic so I'm not sure how useful it would be.

Copy link
Contributor

@manodeep manodeep left a comment

Choose a reason for hiding this comment

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

Made two minor comments - looks good to me

@@ -1622,6 +1622,10 @@ PyWcsprm_cset(

int status = 0;

// We want to avoid calling wcsset whenever possible as it is not thread-safe. We use wcsenq
// to see if the checksum of the wcsprm elements has changed since wcsset was last called.
if (wcsenq(&self->x, WCSENQ_CHK)) return 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be worthwhile to add a comment that wcsenq itself is also not thread-safe, and therefore, should only be invoked while holding the GIL (i.e., not within a threaded region)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just out of curiosity, which part of wcsenq is not thread-safe?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I was totally wrong! I thought chksum was the variable in the wcsprm struct but it is not. chksum is a function local variable and therefore, all threads should compute the same value of chksum in wcsenq

@@ -0,0 +1 @@
Fixed an issue which caused calls to WCS coordinate conversion routines to not be thread-safe due to repeated calls to WCS.wcs.set()
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be good to clarify that memory race was occurring because each thread was calling WCS.wcs.set() in parallel, and not because of repeated calls to wcsset().

@astrofrog astrofrog added the benchmark Run benchmarks for a PR label May 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Multi-threading issue in WCSLIB even if input data is copied
3 participants