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

Add design doc for atomic<T> type. #5101

Merged
merged 5 commits into from
Sep 19, 2024
Merged

Conversation

csyonghe
Copy link
Collaborator

@csyonghe csyonghe commented Sep 18, 2024

Closes #5100.

jkwak-work
jkwak-work previously approved these changes Sep 18, 2024
Copy link
Collaborator

@jkwak-work jkwak-work left a comment

Choose a reason for hiding this comment

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

I loved the writing in the "background" section.
At the end of the section, I was convinced that we should have atomic.

Comment on lines 53 to 60
extension<T> Atomic<T>
where T : IAtomicable
where T : __BuiltinIntegerType
{
[ref] T atomicAnd(T value); // returns original value
[ref] T atomicOr(T value); // returns original value
[ref] T atomicXor(T value); // returns original value
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can add atomicIncrement and atomicDecrement for integer types.

Copy link
Contributor

Choose a reason for hiding this comment

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

Increment/decrement are interesting in that we can provide them via an extension by just doing an add/sub of 1, but we might in principle want allow targets to directly generate a specific increment/decrement op if they support one.

Comment on lines 36 to 37
extension int : IAtomicable {}
extension uint : IAtomicable {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We may want to add 64-bit integer since SM 6.6 supports it.

tangent-vector
tangent-vector previously approved these changes Sep 18, 2024
Copy link
Contributor

@tangent-vector tangent-vector left a comment

Choose a reason for hiding this comment

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

This looks great as a starting point. None of my comments should be taken as a sign that we shouldn't check this in.

One big topic that we'll want to broach in this document (but are unlikely to make any definitive headway on) is: what is Slang's memory model?

Our choice to introduce Atomic<T> is motivated by a desire to start taking memory models seriously, but it's kind of obvious in this document that we aren't making clear statements about how atomic and non-atomic accesses interact, etc. We don't currently spell out the consequences, from the standpoint of Slang's language semantics, for aliasing the same memory as both a RWStructuredBuffer<int> and a RWStructuredBuffer<Atomic<int>> (well, we don't specify any consequences for aliasing of resources in general...).

The hard work on that front needs to happen, but not in this proposal. Still, it would be good for the proposal to note that the hard work still needs to be done for efforts like this to be truly meaningful.


We define an `Atomic<T>` type that functions as a wrapper of `T` and provides atomic operations:
```
interface IAtomicable {}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not a problem to solve with this proposal, but in the long run we really ought to stop using empty interfaces for stuff like this, and then relying on intrinsics that are constrained on the interface.

Ideally, built-in interfaces like this should actually define their requirements explicitly, like:

interface IAtomicable
{
    This atomicLoad( ref Atomic<This> location );
    T atomicExchange( ref Atomic<This> location, This newValue );
    // ...
}

We would then make the Atomic<T> type have entirely concrete method definitions (with hints to ensure they get inlined whenever possible), and the conformances for concrete types like int and uint would then define the required operations explicitly (whether as intrinsics, or using target_switch, etc.).

Such an approach would not only be more "correct," but it also opens the door to having certain types conform in non-intrinsic ways, or even allowing user-defined conformances (e.g., to allow a user-defined type to opt into Atomic<T> support if its in-memory layout is bit-identical to a built-in atomic type).

extension float : IAtomicable {}
extension half : IAtomicable {}

struct Atomic<T>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why isn't there a constraint of T : IAtomicable here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added.

Comment on lines 46 to 50
[ref] T compareExchange(T compareValue, T newValue); // returns old value.
[ref] T atomicAdd(T value); // returns original value
[ref] T atomicSub(T value); // returns original value
[ref] T atomicMax(T value); // returns original value
[ref] T atomicMin(T value); // returns original value
Copy link
Contributor

Choose a reason for hiding this comment

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

In principle, the only truly universal operations that an atomicable type would need to support are load/store and exchange. Compare-exchange relies on the type being comparable (and I'd need to double-check how comparisons are done for floating-point atomic compare-exchange), and the add/sub/min/max operations rely on the type supporting the relevant mathematical operations.

It seems like we might actually need a hierarchy of atomic-related interfaces, representing specific subsets of the available functionality.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added IAtomicable hierarchy.

Comment on lines 53 to 55
extension<T> Atomic<T>
where T : IAtomicable
where T : __BuiltinIntegerType
Copy link
Contributor

Choose a reason for hiding this comment

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

I get what this is doing, but I also worry a little bit, when I think about how this would translate to my "move the required operations into the interface" approach. Effectively, this is saying that any type that conforms IAtomicable & __BuiltinIntegerType has additional requirements beyond those that are stated for either interface alone.

I really do think the right answer here will be a hierarchy of interfaces, along the lines of:

interface IAtomicable { ... } // load/store and exchange
interface IAtomicCompareExchangeable : IAtomicable { ... } // compare-exchange
interface IAtomicNumeric : IAtomicCompareExchangeable { ... } // add/sub/min/max
interface IAtomicLogical : IAtomicCompareExchangeable { ... } // and/or/xor
typealias IAtomicInteger = IAtomicNumeric & IAtomicLogical;

(TBD whether add/sub and min/max should be separated from one another)

(Note: I did not make IAtomicLogical inherit from IAtomicNumeric because it is in principle possible to support Atomic<bool>, which would have logical operations but not add/sub/min/max)

It is likely to be rare for programmers to define their own generics that work with Atomic<T> for various T, so putting the burden on them to specify the range of atomic operations they need to be able to perform doesn't seem like too much.

That said, it is clear that my little hierarchy above closely mirrors the kind of hierarchy we need for builtin scalar types, so it is potentially frustrating to have both IWhatever and IAtomicWhatever as distinct interfaces.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Used hierarchy of IAtomic to clean this up.

Comment on lines 53 to 60
extension<T> Atomic<T>
where T : IAtomicable
where T : __BuiltinIntegerType
{
[ref] T atomicAnd(T value); // returns original value
[ref] T atomicOr(T value); // returns original value
[ref] T atomicXor(T value); // returns original value
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Increment/decrement are interesting in that we can provide them via an extension by just doing an add/sub of 1, but we might in principle want allow targets to directly generate a specific increment/decrement op if they support one.

Comment on lines 63 to 64
We allow `Atomic<T>` to be defined anywhere: as struct fields, as array elements, as elements of `RWStructuredBuffer` types,
or as groupshared variable types. For example, in global memory:
Copy link
Contributor

Choose a reason for hiding this comment

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

This conspicuously doesn't list local variables, function parameters, etc.

Copy link
Collaborator Author

@csyonghe csyonghe Sep 19, 2024

Choose a reason for hiding this comment

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

Added mention of local/global variable and function parameter here.

@csyonghe
Copy link
Collaborator Author

@tangent-vector I updated the doc to reflect the review suggestions.

I am hesitant about moving the requirement into the interfaces, not because they are not better, but because they introduce more implementation complexity than what it worth. Currently, we only plan for T in Atomic<T> to be builtin data types and not any user-defined data types. In this case, all atomic operations are going to map to slang IR opcodes. Doing it per-data-type as implementation to IAtomic means we need to duplicate a lot of the intrinsic_op implementation across all basic types, versus the empty interface implementation allows us to define them in a single centralized place for simplicity.

@csyonghe csyonghe merged commit 1560326 into shader-slang:master Sep 19, 2024
@jeffbolznv
Copy link

One big topic that we'll want to broach in this document (but are unlikely to make any definitive headway on) is: what is Slang's memory model?

IMO, at the very least, this doc should say that these are relaxed atomics and give a rough definition of what that means (no memory ordering is enforced by the instruction).

struct Atomic<T : IAtomicable>
{
T load();
[ref] void store(T newValue); // Question: do we really need this?

Choose a reason for hiding this comment

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

Yes, you surely want this, atomic stores can often be lowered to a much cheaper hardware instruction than whatever you would replace it with (e.g. atomic exchange).


struct Atomic<T : IAtomicable>
{
T load();

Choose a reason for hiding this comment

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

Do you intend to support syntactic sugar where you can use the Atomic as an lvalue/rvalue and assign without using load/store? This is pretty common.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes we will need that.

T load();
[ref] void store(T newValue); // Question: do we really need this?
[ref] T exchange(T newValue); // returns old value
[ref] T compareExchange(T compareValue, T newValue); // returns old value.

Choose a reason for hiding this comment

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

There are sometimes two forms of compareExchange (strong vs weak). Should say which this is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

According to SPIRV spec, the OpAtomicCompareExchangeWeak is deprecated and we only have OpAtomicCompareExchange, so perhaps it is fine to just assume it means weak here?

@csyonghe
Copy link
Collaborator Author

@jeffbolznv The design is updated in PR https://github.com/shader-slang/slang/pull/5125/files
We now have explicit MemoryOrder parameter to specify whether or not the semantics is relaxed or SeqCst.
We can consider adding other enum cases that spirv exposes too.

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.

Atomic<T> design doc.
4 participants