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

#17754: Lower Indestructible to Metal, add guidance on using static vars with non-trivial destructors #17899

Merged
merged 3 commits into from
Feb 20, 2025

Conversation

omilyutin-tt
Copy link
Contributor

@omilyutin-tt omilyutin-tt commented Feb 15, 2025

Ticket

#17754, #17607

Problem description

Variables with static storage duration should have trivial destructors. Add guidance on why this so, and lower Indestructible utility to Metal, as the suggested alternative.

What's changed

  • Lower Indestructible from tt-train to Metal.
  • Add guidance to best practices doc.
  • Add comments and a test for Indestructible.

Checklist

  • All post commit
  • New/Existing tests provide coverage for changes
  • Checked that standalone tt-train compiles.

@@ -1,4 +1,4 @@
# Best Practices for C++20 Repository
# Best Practices for Contributing to TT Metal
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dropping "C++20" part. This is misleading in some cases, for example, the doc suggests to use tt:stl::Span while C++20 has std::span


// Option 2: If dynamic initialization is required, use function-local statics with `Indestructible`.
const auto& get_data() {
static Indestructible<std::string> kData("Disabled destructor! Good!");
Copy link
Contributor

Choose a reason for hiding this comment

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

mb provide another example as seems like static std::string is always bad anyway.

Comment on lines +378 to +384
// Bad: Using a static object with a non-trivial destructor.
static const std::map<int, std::string> kDeviceConfigFiles = {
{1, "n150.yaml"},
{2, "n300.yaml"},
{8, "t3000.yaml"}
};
```
Copy link
Contributor

Choose a reason for hiding this comment

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

What can go wrong in this example? Is it better to include an example with custom destructor like device?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you retain references to either key/value in another static singleton, that other destructor might access it after the memory is freed. This is of course overly simplistic and in isolation looks unrealistic, but just to illustrate the point. Easier to always be safe than to think of when / how things might break.


// Destructor does NOT call T's destructor.
// This leaves the object "indestructible."
~Indestructible() = default;
Copy link
Contributor

Choose a reason for hiding this comment

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

So the moment we start leveraging LeakSanitizer we're going to get flooded with reports?

Why can't we just enforce proper scopes and avoid globals/leaks/UB?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AFAIK asan and leak sanitizers would be happy about it - as memory remains reachable at program exit, while we never run into use-after-free because all dtors are disabled. Having said that, I don't know for sure, but I would be very surprised if sanitizers don't understand this very common pattern.

Why can't we just enforce proper scopes and avoid globals/leaks/UB?

Sometimes it is way easier to use this pattern, basically wherever singleton pattern actually applies, but safer and more efficient.


namespace tt::stl {

// `Indestructible` is a wrapper around `T` that behaves like `T` but does not call the destructor of `T`.
Copy link
Collaborator

@cfjchu cfjchu Feb 19, 2025

Choose a reason for hiding this comment

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

While we're deliberately not calling destructor forT, doesn't this break RAII gurantees if T holds resources like file handles or mutexes and ~T() was supposed to execute some cleanup subroutine?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thats the point:) Static vars shouldn't be responsible for this, as the stuff they are cleaning up (or other dependencies that are needed for this) might be gone already.

Copy link
Contributor

@afuller-TT afuller-TT left a comment

Choose a reason for hiding this comment

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

Approving to unblock as it's clearly a step better than the status quo. Later (unknown timeframe) I intend to circle back to all the ASan/TSan/etc goodness. This may or may not get revisited at that time.

@omilyutin-tt omilyutin-tt merged commit 42cf08b into main Feb 20, 2025
12 checks passed
@omilyutin-tt omilyutin-tt deleted the omilyutin/static branch February 20, 2025 19:20
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.

5 participants