-
Notifications
You must be signed in to change notification settings - Fork 18.1k
[mlir][GPU] Extend gpu.barrier with scope and named-barrier support #195692
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
Changes from all commits
5d6b29f
bf035d9
2d12240
b73d9a7
bda56a0
5c7b00e
567c92a
9662585
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,14 @@ class AsyncTokenType | |
| static constexpr StringLiteral name = "gpu.async_token"; | ||
| }; | ||
|
|
||
| class NamedBarrierType | ||
| : public Type::TypeBase<NamedBarrierType, Type, TypeStorage> { | ||
| public: | ||
| using Base::Base; | ||
|
|
||
| static constexpr StringLiteral name = "gpu.named_barrier"; | ||
| }; | ||
|
|
||
|
Comment on lines
+54
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you define it in C++ and not td?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's how the dialect does things? |
||
| /// MMAMatrixType storage and uniquing. Array is uniqued based on its shape | ||
| /// and type. | ||
| struct MMAMatrixStorageType : public TypeStorage { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1427,13 +1427,21 @@ def GPU_RotateOp : GPU_Op< | |
| } | ||
|
|
||
| def GPU_BarrierOp : GPU_Op<"barrier">, | ||
| Arguments<(ins OptionalAttr<GPU_AddressSpaceAttrArray> :$address_spaces)> { | ||
| let summary = "Synchronizes all work items of a workgroup."; | ||
| Arguments<(ins | ||
| OptionalAttr<GPU_AddressSpaceAttrArray>:$address_spaces, | ||
| Optional<GPU_NamedBarrier>:$named_barrier, | ||
| DefaultValuedAttr<GPU_BarrierScopeAttr, | ||
| "::mlir::gpu::BarrierScope::Workgroup">:$scope | ||
| )> { | ||
| let summary = "Synchronizes work items within an execution scope."; | ||
| let description = [{ | ||
| The `barrier` op synchronizes all work items of a workgroup. It is used | ||
| to coordinate communication between the work items of the workgroup. | ||
| The `barrier` op synchronizes work items within the specified execution | ||
| scope. By default, the scope is `workgroup`, synchronizing all work items | ||
| in a workgroup. | ||
|
|
||
| ```mlir | ||
| // Synchronize all work items in the workgroup, making all prior | ||
| // memory accesses visible. | ||
| gpu.barrier | ||
| ``` | ||
|
|
||
|
|
@@ -1443,35 +1451,94 @@ def GPU_BarrierOp : GPU_Op<"barrier">, | |
| accessing the same memory can be avoided by synchronizing work items | ||
| in-between these accesses. | ||
|
|
||
| If the `memfence` attribute is specified, the set of memory accesses that must | ||
| by completed after the barrier resolves is limited to only those accesses that | ||
| read from or write to the specified address spaces (though accesses to other | ||
| address spaces may be completed as well, especially if a particular combination | ||
| of address spaces is not supported on a given backend). In particular, | ||
| specifying `memfence []` creates a barrier that is not required to affect | ||
| the visibility of any memory operations and is purely used for synchronizing | ||
| work items. | ||
| The `scope` attribute controls the execution scope of the barrier: | ||
|
|
||
| ```mlir | ||
| // Only workgroup address spaces accesses required to be visible. | ||
| // Synchronize within a subgroup (warp/wavefront). | ||
| gpu.barrier scope <subgroup> | ||
| // Synchronize within a cluster. | ||
| gpu.barrier scope <cluster> | ||
| ``` | ||
|
|
||
| A `named` barrier allows synchronizing a specific subset of subgroups | ||
| that have been associated with a named barrier handle. Named barriers | ||
| require workgroup scope. | ||
|
Comment on lines
+1464
to
+1465
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In view of https://developer.nvidia.com/blog/cooperative-groups/ the requirement of workgroup seems arbitrary.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we've got named barriers at cluster scope, I'm down to withdraw that restriction.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I meant is, NVIDIA appears to support thread barriers that are smaller than a warp/wave. @grypp , thoughts?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think nvvm.barrier requires the member count to be a multiple of the subgorup size? Unless that's been cleaned up since the documentation I looked at?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, @fabianmcg is right. At this point, you could synchronize almost anything—sub-warp, cluster, sub-thread block, etc.—using mbarrier or some strange atomic tricks. I think we should focus on supporting named barriers, which are a single instruction ( |
||
|
|
||
| ```mlir | ||
| // Initialize a named barrier for 4 participating members. | ||
| %nb = gpu.initialize_named_barrier %c4 : i32 -> !gpu.named_barrier | ||
| // Wait on the named barrier. | ||
| gpu.barrier named(%nb : !gpu.named_barrier) | ||
| ``` | ||
|
|
||
| If the `memfence` attribute is specified, the set of memory accesses that | ||
| must be completed after the barrier resolves is limited to only those | ||
| accesses that read from or write to the specified address spaces. In | ||
| particular, specifying `memfence []` creates a barrier that is not required | ||
| to affect the visibility of any memory operations and is purely used for | ||
| synchronizing work items. | ||
|
|
||
| ```mlir | ||
| // Only workgroup address space accesses required to be visible. | ||
| gpu.barrier memfence [#gpu.address_space<workgroup>] | ||
| // No memory accesses required to be visible. | ||
| gpu.barrier memfence [] | ||
| // All memory accesses required to be visible. | ||
| gpu.barrier | ||
| ``` | ||
|
|
||
| Either none or all work items of a workgroup need to execute this op | ||
| in convergence. | ||
| The three clauses can be combined in any order, but not all combinations may | ||
| be supported on a given target: | ||
|
|
||
| ```mlir | ||
| // Named barrier with a workgroup-only memory fence. | ||
| gpu.barrier named(%nb : !gpu.named_barrier) memfence [#gpu.address_space<workgroup>] | ||
| // Subgroup barrier with a global fence. | ||
| gpu.barrier memfence [#gpu.address_space<global>] scope <subgroup> | ||
| ``` | ||
|
Comment on lines
+1490
to
+1498
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks too generic, can we scope it a bit? eg. named barriers cannot be used in conjunction with scopes. We can relax this requirement when it proves necessary.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to keep the options, and would like to to note that on AMD, named barriers can darn well be used with memfence lists (the only current catch is that they need to be workgroup-scoped)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was objecting named barriers and scope. Not the memfence + (named or scope).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Named barriers + scope is going to be cluster-scoped named barriers on gfx[eventually] (I forget if it's a gfx1250 or not) |
||
|
|
||
| Once one thread of execution in a given scope (say, thread in a workgroup) | ||
| has executed a particular dynamic instance of `gpu.barrier`, all other threads | ||
| in that scope are required to execute the same dynamic instance of `gpu.barrier` | ||
| before any thread executes any other instance of it. That is, you cannot, for | ||
| example, have the two subgroups of a workgroup arrive at `gpu.barrier` ops in | ||
| different branches of an if statement and have this work. | ||
| }]; | ||
| let assemblyFormat = [{ | ||
| oilist( | ||
| `named` `(` $named_barrier `:` type($named_barrier) `)` | ||
| | `memfence` $address_spaces | ||
| | `scope` $scope | ||
| ) attr-dict | ||
| }]; | ||
| let assemblyFormat = "(`memfence` $address_spaces^)? attr-dict"; | ||
| let hasCanonicalizer = 1; | ||
| let hasVerifier = 1; | ||
| let builders = [OpBuilder<( | ||
| ins CArg<"std::optional<::mlir::gpu::AddressSpace>", | ||
| "std::nullopt">:$addressSpace)>, | ||
| OpBuilder<(ins "Value":$memrefToFence)>]; | ||
| } | ||
|
|
||
| def GPU_InitializeNamedBarrierOp | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PTX has had named barriers via What would
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does y'all's LLVM backend not have a thing where it'll allocate the IDs for you?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If not, we'd probably want to lower to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, named barriers are pre-allocated in hardware, so we have a single instruction for that.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with the current design is that it requires use-def analysis to recover the barrier id and thread count. This isn’t always possible: a named barrier type can be produced in one function and passed to another, at which point the def-chain is broken and the information is lost.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once could have id and participants on the IR, like This way you lose the ability to express a runtime-computed participant count, because types can only carry attributes (constants), not SSA values. But this info is typically known at codegen time.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is my implementation proof enough that the current design works? |
||
| : GPU_Op<"initialize_named_barrier", | ||
| [MemoryEffects<[MemAlloc<DefaultResource>]>]> { | ||
| let summary = "Initialize a named barrier with a member count."; | ||
| let description = [{ | ||
| Initializes a named barrier object with the given number of participating | ||
| members (subgroups) and returns a handle to it. All members that will | ||
|
krzysz00 marked this conversation as resolved.
|
||
| synchronize on this barrier must be accounted for in the count. | ||
|
|
||
| ```mlir | ||
| %nb = gpu.initialize_named_barrier %num_members : i32 -> !gpu.named_barrier | ||
| ``` | ||
| }]; | ||
| let arguments = (ins I32:$member_count); | ||
| let results = (outs GPU_NamedBarrier:$result); | ||
| let assemblyFormat = [{ | ||
| $member_count attr-dict `:` type($member_count) `->` type($result) | ||
| }]; | ||
| } | ||
|
|
||
| def GPU_GPUModuleOp : GPU_Op<"module", [ | ||
| IsolatedFromAbove, DataLayoutOpInterface, HasDefaultDLTIDataLayout, | ||
| NoRegionArguments, SymbolTable, Symbol] # GraphRegionNoTerminator.traits> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using dialect type here is weird, either define the type in ODS or use simpler TypeConstraint.