Skip to content

[mlir][GPU] Extend gpu.barrier with scope and named-barrier support - #195692

Merged
krzysz00 merged 8 commits into
llvm:mainfrom
krzysz00:gpu-named-barrier
May 18, 2026
Merged

[mlir][GPU] Extend gpu.barrier with scope and named-barrier support#195692
krzysz00 merged 8 commits into
llvm:mainfrom
krzysz00:gpu-named-barrier

Conversation

@krzysz00

@krzysz00 krzysz00 commented May 4, 2026

Copy link
Copy Markdown
Contributor

This commit adds two features to gpu.barrier that are supported on targets like recent AMDGPU chips and SPIR-V.

The first of these is named barriers, which allow creating a barrier object that is initialized with the number of subgroups that must arrive at it before those subgroups are released. These are represented in MLIR with a new !gpu.named_barrier type and created by gpu.initialized_named_barrier operation. These named barriers then become arguments to gpu.barrier.

The other change is adding a "scope" enum and using it to specify the execution scope of barriers. This allows for rerpresenting cluster- and subgroup-wide barriers (the latter exists on AMDGPU and Nvidia, and while I suspect Nvidia has cluster-scope barriers, I didn't go looking) and allows us to fully lower to SPIR-V's OpControlBarrier.

While these are two different features, I figured I'd land them in one PR so the full API for gpu.barrier only changes once.

@llvmorg-github-actions

llvmorg-github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-backend-amdgpu
@llvm/pr-subscribers-mlir-spirv
@llvm/pr-subscribers-mlir

@llvm/pr-subscribers-mlir-gpu

Author: Krzysztof Drewniak (krzysz00)

Changes

This commit adds two features to gpu.barrier that are supported on targets like recent AMDGPU chips and SPIR-V.

The first of these is named barriers, which allow creating a barrier object that is initialized with the number of subgroups that must arrive at it before those subgroups are released. These are represented in MLIR with a new !gpu.named_barrier type and created by gpu.initialized_named_barrier operation. These named barriers then become arguments to gpu.barrier.

The other change is adding a "scope" enum and using it to specify the execution scope of barriers. This allows for rerpresenting cluster- and subgroup-wide barriers (the latter exists on AMDGPU and Nvidia, and while I suspect Nvidia has cluster-scope barriers, I didn't go looking) and allows us to fully lower to SPIR-V's OpControlBarrier.

While these are two different features, I figured I'd land them in one PR so the full API for gpu.barrier only changes once.


Patch is 44.64 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/195692.diff

19 Files Affected:

  • (modified) mlir/include/mlir/Conversion/GPUToSPIRV/GPUToSPIRV.h (+4)
  • (modified) mlir/include/mlir/Dialect/GPU/IR/GPUBase.td (+28)
  • (modified) mlir/include/mlir/Dialect/GPU/IR/GPUDialect.h (+8)
  • (modified) mlir/include/mlir/Dialect/GPU/IR/GPUOps.td (+81-16)
  • (modified) mlir/lib/Conversion/GPUToNVVM/GPUToNVVM.td (-2)
  • (modified) mlir/lib/Conversion/GPUToNVVM/LowerGpuOpsToNVVMOps.cpp (+33-2)
  • (modified) mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp (+179-52)
  • (modified) mlir/lib/Conversion/GPUToSPIRV/GPUToSPIRV.cpp (+72-6)
  • (modified) mlir/lib/Conversion/GPUToSPIRV/GPUToSPIRVPass.cpp (+1)
  • (modified) mlir/lib/Dialect/GPU/IR/GPUDialect.cpp (+27-1)
  • (modified) mlir/lib/Dialect/GPU/Transforms/EliminateBarriers.cpp (+6)
  • (modified) mlir/test/Conversion/GPUToNVVM/gpu-to-nvvm.mlir (+8)
  • (modified) mlir/test/Conversion/GPUToROCDL/gpu-to-rocdl-barrier.mlir (+18)
  • (added) mlir/test/Conversion/GPUToROCDL/gpu-to-rocdl-barriers-gfx12.mlir (+54)
  • (added) mlir/test/Conversion/GPUToROCDL/gpu-to-rocdl-invalid-named-barrier.mlir (+10)
  • (added) mlir/test/Conversion/GPUToROCDL/gpu-to-rocdl-named-barrier-non-const.mlir (+10)
  • (modified) mlir/test/Conversion/GPUToSPIRV/gpu-to-spirv.mlir (+38)
  • (modified) mlir/test/Dialect/GPU/canonicalize.mlir (+45)
  • (added) mlir/test/Dialect/GPU/named-barrier.mlir (+77)
diff --git a/mlir/include/mlir/Conversion/GPUToSPIRV/GPUToSPIRV.h b/mlir/include/mlir/Conversion/GPUToSPIRV/GPUToSPIRV.h
index 825142d120587..75d3050bc95d9 100644
--- a/mlir/include/mlir/Conversion/GPUToSPIRV/GPUToSPIRV.h
+++ b/mlir/include/mlir/Conversion/GPUToSPIRV/GPUToSPIRV.h
@@ -35,6 +35,10 @@ void populateGpuWMMAToSPIRVCoopMatrixKHRConversionPatterns(
 /// conversion to the type converter.
 void populateMMAToSPIRVCoopMatrixTypeConversion(
     SPIRVTypeConverter &typeConverter);
+
+/// Adds `gpu::NamedBarrierType` to `spirv::NamedBarrierType` conversion.
+void populateGPUNamedBarrierToSPIRVTypeConversion(
+    SPIRVTypeConverter &typeConverter);
 } // namespace mlir
 
 #endif // MLIR_CONVERSION_GPUTOSPIRV_GPUTOSPIRV_H
diff --git a/mlir/include/mlir/Dialect/GPU/IR/GPUBase.td b/mlir/include/mlir/Dialect/GPU/IR/GPUBase.td
index 49ecc7f6c9b95..68f06b625170e 100644
--- a/mlir/include/mlir/Dialect/GPU/IR/GPUBase.td
+++ b/mlir/include/mlir/Dialect/GPU/IR/GPUBase.td
@@ -115,6 +115,29 @@ def GPU_AddressSpaceAttr :
 
 def GPU_AddressSpaceAttrArray : TypedArrayAttrBase<GPU_AddressSpaceAttr, "GPU Address Space array">;
 
+def GPU_ScopeThread      : I32EnumCase<"Thread",      0, "thread">;
+def GPU_ScopeSubgroup    : I32EnumCase<"Subgroup",    1, "subgroup">;
+def GPU_ScopeWorkgroup   : I32EnumCase<"Workgroup",   2, "workgroup">;
+def GPU_ScopeCluster     : I32EnumCase<"Cluster",     3, "cluster">;
+def GPU_ScopeDevice      : I32EnumCase<"Device",      4, "device">;
+def GPU_ScopeCrossDevice : I32EnumCase<"CrossDevice", 5, "cross_device">;
+
+def GPU_ScopeEnum : I32Enum<
+  "Scope", "barrier execution scope", [
+    GPU_ScopeThread,
+    GPU_ScopeSubgroup,
+    GPU_ScopeWorkgroup,
+    GPU_ScopeCluster,
+    GPU_ScopeDevice,
+    GPU_ScopeCrossDevice
+  ]> {
+  let cppNamespace = "::mlir::gpu";
+}
+
+def GPU_ScopeAttr : EnumAttr<GPU_Dialect, GPU_ScopeEnum, "scope"> {
+  let assemblyFormat = "`<` $value `>`";
+}
+
 def GPU_Dimension : GPU_I32Enum<"Dimension",
     "a dimension, either 'x', 'y', or 'z'",
     [
@@ -169,6 +192,11 @@ def GPU_SparseDnTensorHandle : GPU_SparseHandle<"SparseDnTensorHandleType", "den
 def GPU_SparseSpGEMMOpHandle : GPU_SparseHandle<"SparseSpGEMMOpHandleType", "SpGEMM operation">;
 def GPU_SparseSpMatHandle : GPU_SparseHandle<"SparseSpMatHandleType", "sparse matrix">;
 
+def GPU_NamedBarrier : DialectType<
+  GPU_Dialect,
+  CPred<"::llvm::isa<::mlir::gpu::NamedBarrierType>($_self)">,
+  "named barrier type">,
+  BuildableType<"mlir::gpu::NamedBarrierType::get($_builder.getContext())">;
 
 //===----------------------------------------------------------------------===//
 // GPU Interfaces.
diff --git a/mlir/include/mlir/Dialect/GPU/IR/GPUDialect.h b/mlir/include/mlir/Dialect/GPU/IR/GPUDialect.h
index 6e9cf709c7585..0e135781d9d92 100644
--- a/mlir/include/mlir/Dialect/GPU/IR/GPUDialect.h
+++ b/mlir/include/mlir/Dialect/GPU/IR/GPUDialect.h
@@ -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";
+};
+
 /// MMAMatrixType storage and uniquing. Array is uniqued based on its shape
 /// and type.
 struct MMAMatrixStorageType : public TypeStorage {
diff --git a/mlir/include/mlir/Dialect/GPU/IR/GPUOps.td b/mlir/include/mlir/Dialect/GPU/IR/GPUOps.td
index a5525580fb320..a9df774ba76c7 100644
--- a/mlir/include/mlir/Dialect/GPU/IR/GPUOps.td
+++ b/mlir/include/mlir/Dialect/GPU/IR/GPUOps.td
@@ -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_ScopeAttr,
+        "::mlir::gpu::Scope::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,17 +1451,35 @@ 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 across the entire device.
+    gpu.barrier scope <device>
+    ```
+
+    A `named` barrier allows synchronizing a specific subset of subgroups
+    that have been associated with a named barrier handle. Named barriers
+    require workgroup scope.
+
+    ```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 []
@@ -1461,17 +1487,56 @@ def GPU_BarrierOp : GPU_Op<"barrier">,
     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>
+    ```
+
+    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 must execute the same dynamic instance of `gpu.barrier` before
+    executing any other instance of it.
+  }];
+  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
+    : 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
+    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> {
diff --git a/mlir/lib/Conversion/GPUToNVVM/GPUToNVVM.td b/mlir/lib/Conversion/GPUToNVVM/GPUToNVVM.td
index 0fcda38631a9b..b2288938fec22 100644
--- a/mlir/lib/Conversion/GPUToNVVM/GPUToNVVM.td
+++ b/mlir/lib/Conversion/GPUToNVVM/GPUToNVVM.td
@@ -17,6 +17,4 @@ include "mlir/IR/PatternBase.td"
 include "mlir/Dialect/GPU/IR/GPUOps.td"
 include "mlir/Dialect/LLVMIR/NVVMOps.td"
 
-def : Pat<(GPU_BarrierOp : $op $memory_fence), (NVVM_Barrier0Op)>;
-
 #endif // MLIR_CONVERSION_GPUTONVVM_TD
diff --git a/mlir/lib/Conversion/GPUToNVVM/LowerGpuOpsToNVVMOps.cpp b/mlir/lib/Conversion/GPUToNVVM/LowerGpuOpsToNVVMOps.cpp
index 44a4cafcc224b..02b71a638c108 100644
--- a/mlir/lib/Conversion/GPUToNVVM/LowerGpuOpsToNVVMOps.cpp
+++ b/mlir/lib/Conversion/GPUToNVVM/LowerGpuOpsToNVVMOps.cpp
@@ -370,6 +370,37 @@ struct AssertOpToAssertfailLowering
 /// Import the GPU Ops to NVVM Patterns.
 #include "GPUToNVVM.cpp.inc"
 
+struct GPUBarrierOpNVVMLowering final
+    : public ConvertOpToLLVMPattern<gpu::BarrierOp> {
+  using ConvertOpToLLVMPattern::ConvertOpToLLVMPattern;
+
+  LogicalResult
+  matchAndRewrite(gpu::BarrierOp op, gpu::BarrierOp::Adaptor adaptor,
+                  ConversionPatternRewriter &rewriter) const override {
+    if (op.getNamedBarrier())
+      return rewriter.notifyMatchFailure(
+          op, "named barriers are not supported on NVVM");
+
+    gpu::Scope scope = op.getScope();
+    switch (scope) {
+    case gpu::Scope::Workgroup:
+      rewriter.replaceOpWithNewOp<NVVM::Barrier0Op>(op);
+      return success();
+    case gpu::Scope::Subgroup: {
+      // Emit __syncwarp(0xFFFFFFFF) for full-warp sync.
+      Value mask = LLVM::ConstantOp::create(
+          rewriter, op.getLoc(), rewriter.getI32Type(),
+          rewriter.getI32IntegerAttr(0xFFFFFFFF));
+      rewriter.replaceOpWithNewOp<NVVM::SyncWarpOp>(op, mask);
+      return success();
+    }
+    default:
+      return rewriter.notifyMatchFailure(
+          op, "unsupported scope for NVVM barrier lowering");
+    }
+  }
+};
+
 /// A pass that replaces all occurrences of GPU device operations with their
 /// corresponding NVVM equivalent.
 ///
@@ -506,8 +537,8 @@ void mlir::populateGpuToNVVMConversionPatterns(
   // TODO: Pass benefit to generated patterns.
   populateWithGenerated(patterns);
 
-  patterns.add<GPUPrintfOpToVPrintfLowering, AssertOpToAssertfailLowering>(
-      converter, benefit);
+  patterns.add<GPUBarrierOpNVVMLowering, GPUPrintfOpToVPrintfLowering,
+               AssertOpToAssertfailLowering>(converter, benefit);
   patterns.add<
       gpu::index_lowering::OpLowering<gpu::ThreadIdOp, NVVM::ThreadIdXOp,
                                       NVVM::ThreadIdYOp, NVVM::ThreadIdZOp>>(
diff --git a/mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp b/mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp
index 58ab1c799b574..39810e498f6a7 100644
--- a/mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp
+++ b/mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp
@@ -36,6 +36,7 @@
 #include "mlir/Dialect/MemRef/IR/MemRef.h"
 #include "mlir/Dialect/Vector/IR/VectorOps.h"
 #include "mlir/IR/BuiltinAttributes.h"
+#include "mlir/IR/Matchers.h"
 #include "mlir/Transforms/DialectConversion.h"
 #include "mlir/Transforms/GreedyPatternRewriteDriver.h"
 
@@ -517,6 +518,51 @@ struct GPUShuffleOpLowering : public ConvertOpToLLVMPattern<gpu::ShuffleOp> {
   }
 };
 
+/// Emit an LLVM fence with MMRA metadata based on the given address spaces.
+/// If `addrSpaces` is nullopt, all memory is fenced (global + LDS).
+static void emitFences(std::optional<ArrayAttr> addrSpaces,
+                       ConversionPatternRewriter &rewriter, Location loc,
+                       StringRef scope, bool before) {
+  bool fenceGlobal = false;
+  bool fenceLDS = false;
+
+  if (addrSpaces) {
+    for (auto spaceAttr : addrSpaces->getAsRange<gpu::AddressSpaceAttr>()) {
+      switch (spaceAttr.getValue()) {
+      case gpu::AddressSpace::Global:
+        fenceGlobal = true;
+        break;
+      case gpu::AddressSpace::Workgroup:
+        fenceLDS = true;
+        break;
+      case gpu::AddressSpace::Private:
+      case gpu::AddressSpace::Constant:
+        break;
+      }
+    }
+  } else {
+    fenceGlobal = true;
+    fenceLDS = true;
+  }
+
+  if (!fenceGlobal && !fenceLDS)
+    return;
+
+  Attribute mmra;
+  if (fenceLDS && !fenceGlobal)
+    mmra =
+        rewriter.getAttr<LLVM::MMRATagAttr>("amdgpu-synchronize-as", "local");
+  else if (fenceGlobal && !fenceLDS)
+    mmra = rewriter.getAttr<LLVM::MMRATagAttr>("amdgpu-synchronize-as",
+                                               "global");
+
+  auto ordering = before ? LLVM::AtomicOrdering::release
+                         : LLVM::AtomicOrdering::acquire;
+  auto fence = LLVM::FenceOp::create(rewriter, loc, ordering, scope);
+  if (mmra)
+    fence->setDiscardableAttr(LLVM::LLVMDialect::getMmraAttrName(), mmra);
+}
+
 struct GPUBarrierOpLowering final : ConvertOpToLLVMPattern<gpu::BarrierOp> {
   GPUBarrierOpLowering(const LLVMTypeConverter &converter,
                        amdgpu::Chipset chipset)
@@ -528,71 +574,147 @@ struct GPUBarrierOpLowering final : ConvertOpToLLVMPattern<gpu::BarrierOp> {
   matchAndRewrite(gpu::BarrierOp op, gpu::BarrierOp::Adaptor adaptor,
                   ConversionPatternRewriter &rewriter) const override {
     Location loc = op.getLoc();
-
-    // Analyze the address_spaces attribute to determine fence behavior.
-    bool fenceGlobal = false;
-    bool fenceLDS = false;
-    std::optional<ArrayAttr> addrSpacesToFence = op.getAddressSpaces();
-
-    if (addrSpacesToFence) {
-      for (auto spaceAttr :
-           addrSpacesToFence->getAsRange<gpu::AddressSpaceAttr>()) {
-        switch (spaceAttr.getValue()) {
-        case gpu::AddressSpace::Global:
-          fenceGlobal = true;
-          break;
-        case gpu::AddressSpace::Workgroup:
-          fenceLDS = true;
-          break;
-        case gpu::AddressSpace::Private:
-        case gpu::AddressSpace::Constant:
-          // Private is thread-local, constant is read-only; no fencing needed.
-          break;
-        }
-      }
-    } else {
-      // Default semantics match __syncthreads() and fence both global and LDS.
-      fenceGlobal = true;
-      fenceLDS = true;
+    gpu::Scope scope = op.getScope();
+
+    // Subgroup (wave) scope.
+    if (scope == gpu::Scope::Subgroup) {
+      emitFences(op.getAddressSpaces(), rewriter, loc, "wavefront",
+                 /*before=*/true);
+      ROCDL::WaveBarrierOp::create(rewriter, loc);
+      emitFences(op.getAddressSpaces(), rewriter, loc, "wavefront",
+                 /*before=*/false);
+      rewriter.eraseOp(op);
+      return success();
     }
 
-    Attribute mmra;
-    if (fenceLDS && !fenceGlobal) {
-      mmra =
-          rewriter.getAttr<LLVM::MMRATagAttr>("amdgpu-synchronize-as", "local");
-    } else if (fenceGlobal && !fenceLDS) {
-      mmra = rewriter.getAttr<LLVM::MMRATagAttr>("amdgpu-synchronize-as",
-                                                 "global");
+    // Device and CrossDevice scopes are not directly representable.
+    if (scope == gpu::Scope::Device || scope == gpu::Scope::CrossDevice)
+      return op.emitOpError("device/cross_device scope barriers are not "
+                            "supported on AMDGPU");
+
+    // Cluster scope: gfx1250+ only, signal/wait with constant -3.
+    if (scope == gpu::Scope::Cluster) {
+      if (chipset < amdgpu::Chipset(12, 5, 0))
+        return op.emitOpError("cluster scope barriers require gfx1250+");
+      emitFences(op.getAddressSpaces(), rewriter, loc, "cluster",
+                 /*before=*/true);
+      ROCDL::BarrierSignalOp::create(rewriter, loc, -3);
+      ROCDL::BarrierWaitOp::create(rewriter, loc,
+                                   static_cast<int16_t>(-3));
+      emitFences(op.getAddressSpaces(), rewriter, loc, "cluster",
+                 /*before=*/false);
+      rewriter.eraseOp(op);
+      return success();
     }
 
-    constexpr llvm::StringLiteral scope = "workgroup";
-
-    bool emitFences = fenceGlobal || fenceLDS;
-    // Emit release fence if needed.
-    if (emitFences) {
-      auto relFence = LLVM::FenceOp::create(
-          rewriter, loc, LLVM::AtomicOrdering::release, scope);
-      if (mmra)
-        relFence->setDiscardableAttr(LLVM::LLVMDialect::getMmraAttrName(),
-                                     mmra);
+    // Workgroup scope (default).
+    assert(scope == gpu::Scope::Workgroup);
+
+    // Named barrier path.
+    if (Value namedBarrier = adaptor.getNamedBarrier()) {
+      if (chipset.majorVersion < 12)
+        return op.emitOpError("named barriers require gfx12+");
+
+      emitFences(op.getAddressSpaces(), rewriter, loc, "workgroup",
+                 /*before=*/true);
+      // A wave must join the named barrier before it may signal it.
+      ROCDL::BarrierJoinOp::create(rewriter, loc, namedBarrier);
+      // Signal with memberCnt=0 retains the count from s.barrier.init.
+      ROCDL::BarrierSignalVarOp::create(rewriter, loc, namedBarrier,
+                                        /*memberCnt=*/0);
+      // id=1 selects the named-barrier wait class; the actual barrier waited
+      // on is the last one this wave joined.
+      ROCDL::BarrierWaitOp::create(rewriter, loc, static_cast<int16_t>(1));
+      emitFences(op.getAddressSpaces(), rewriter, loc, "workgroup",
+                 /*before=*/false);
+      rewriter.eraseOp(op);
+      return success();
     }
 
+    // Regular workgroup barrier.
+    emitFences(op.getAddressSpaces(), rewriter, loc, "workgroup",
+               /*before=*/true);
     if (chipset.majorVersion < 12) {
       ROCDL::SBarrierOp::create(rewriter, loc);
     } else {
       ROCDL::BarrierSignalOp::create(rewriter, loc, -1);
-      ROCDL::BarrierWaitOp::create(rewriter, loc, -1);
+      ROCDL::BarrierWaitOp::create(rewriter, loc, static_cast<int16_t>(-1));
     }
+    emitFences(op.getAddressSpaces(), rewriter, loc, "workgroup",
+               /*before=*/false);
+    rewriter.eraseOp(op);
+    return success();
+  }
+};
+
+struct GPUInitializeNamedBarrierOpLowering final
+    : ConvertOpToLLVMPattern<gpu::InitializeNamedBarrierOp> {
+  GPUInitializeNamedBarrierOpLowering(const LLVMTypeConverter &converter,
+                                      amdgpu::Chipset chipset)
+      : ConvertOpToLLVMPattern<gpu::InitializeNamedBarrierOp>(converter),
+        chipset(chipset) {}
+
+  amdgpu::Chipset chipset;
+
+  LogicalResult
+  matchAndRewrite(gpu::InitializeNamedBarrierOp op,
+                  gpu::InitializeNamedBarrierOp::Adaptor adaptor,
+                  ConversionPatternRewriter &rewriter) const override {
+    if (chipset.majorVersion < 12)
+      return op.emitOpError("named barriers require gfx12+");
+
+    Location loc = op.getLoc();
 
-    if (emitFences) {
-      auto acqFence = LLVM::FenceOp::create(
-          rewriter, loc, LLVM::AtomicOrdering::acquire, scope);
-      if (mmra)
-        acqFence->setDiscardableAttr(LLVM::LLVMDialect::getMmraAttrName(),
-                                     mmra);
+    // The count must be a constant for rocdl.s.barrier.init.
+    IntegerAttr countAttr;
+    if (!matchPattern(op.getMemberCount(), m_Const...
[truncated]

@krzysz00

krzysz00 commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

( @grypp You're here for the Nvidia changes, and I hope I grabbed the right person for those )

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

Comment thread mlir/lib/Conversion/GPUToNVVM/LowerGpuOpsToNVVMOps.cpp Outdated
Comment thread mlir/lib/Conversion/GPUToNVVM/LowerGpuOpsToNVVMOps.cpp Outdated
OpBuilder<(ins "Value":$memrefToFence)>];
}

def GPU_InitializeNamedBarrierOp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

PTX has had named barriers via bar.sync a, b for a long time, and the nvvm.barrier op already exposes them as barrierId + numberOfThreads. Has lowering gpu.barrier named(%nb) to that been considered?

What would gpu.initialize_named_barrier lower to on NVVM, given there's no init op in PTX?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If not, we'd probably want to lower to {i32, i32} where the first i32is the value of some constant global symbol we conjure up and the second is the member count * the size of of the subgroup.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once could have id and participants on the IR, like

gpu.named_barrier.init -> !gpu.named_barrier<id = 3, count = 256>


...

func.func @consumer(%nb : !gpu.named_barrier<id = 3, count = 256>) 
{
  // lowers to nvvm.barrier %c3, %c256
  gpu.barrier %nb 
}

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is my implementation proof enough that the current design works?

Comment thread mlir/include/mlir/Dialect/GPU/IR/GPUOps.td
Comment thread mlir/include/mlir/Dialect/GPU/IR/GPUBase.td Outdated
Comment thread mlir/include/mlir/Dialect/GPU/IR/GPUBase.td Outdated
Comment thread mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp Outdated
Comment thread mlir/lib/Dialect/GPU/Transforms/EliminateBarriers.cpp
Comment thread mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp
krzysz00 and others added 2 commits May 5, 2026 20:51
This commit adds two features to gpu.barrier that are supported on
targets like recent AMDGPU chips and SPIR-V.

The first of these is named barriers, which allow creating a barrier
object that is initialized with the number of subgroups that must
arrive at it before those subgroups are released. These are
represented in MLIR with a new `!gpu.named_barrier` type and created
by `gpu.initialized_named_barrier` operation. These named barriers
then become arguments to `gpu.barrier`.

The other change is adding a "scope" enum and using it to specify the
execution scope of barriers. This allows for rerpresenting cluster-
and subgroup-wide barriers (the latter exists on AMDGPU and Nvidia,
and while I suspect Nvidia has cluster-scope barriers, I didn't go
looking) and allows us to fully lower to SPIR-V's OpControlBarrier.

While there are two different features, I figured I'd land them in one
PR so the full API for `gpu.barrier` only changes once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

def GPU_ScopeThread : I32EnumCase<"Thread", 0, "thread">;
def GPU_ScopeSubgroup : I32EnumCase<"Subgroup", 1, "subgroup">;
def GPU_ScopeWorkgroup : I32EnumCase<"Workgroup", 2, "workgroup">;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could rename this ThreadBlock to be consistent with the remaining of GPU dialect

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Except that we've got some workgroup running around - in the address spaces, for one thing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But I don't have a strong opinion here (my weak opinion is we should rename blocks to workgroups but I don't want to break the world about it)

OpBuilder<(ins "Value":$memrefToFence)>];
}

def GPU_InitializeNamedBarrierOp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

OpBuilder<(ins "Value":$memrefToFence)>];
}

def GPU_InitializeNamedBarrierOp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

OpBuilder<(ins "Value":$memrefToFence)>];
}

def GPU_InitializeNamedBarrierOp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once could have id and participants on the IR, like

gpu.named_barrier.init -> !gpu.named_barrier<id = 3, count = 256>


...

func.func @consumer(%nb : !gpu.named_barrier<id = 3, count = 256>) 
{
  // lowers to nvvm.barrier %c3, %c256
  gpu.barrier %nb 
}

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.

Comment on lines +54 to +61
class NamedBarrierType
: public Type::TypeBase<NamedBarrierType, Type, TypeStorage> {
public:
using Base::Base;

static constexpr StringLiteral name = "gpu.named_barrier";
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do you define it in C++ and not td?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's how the dialect does things?

Comment on lines +195 to +199
def GPU_NamedBarrier : DialectType<
GPU_Dialect,
CPred<"::llvm::isa<::mlir::gpu::NamedBarrierType>($_self)">,
"named barrier type">,
BuildableType<"mlir::gpu::NamedBarrierType::get($_builder.getContext())">;

Copy link
Copy Markdown
Contributor

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.

Comment on lines +1464 to +1465
that have been associated with a named barrier handle. Named barriers
require workgroup scope.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 (bar.sync), and error out for everything else for the time being

Comment on lines +1490 to +1498
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>
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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)

Comment thread mlir/include/mlir/Dialect/GPU/IR/GPUBase.td Outdated
Comment thread mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp Outdated
Comment thread mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp
Comment thread mlir/lib/Conversion/GPUToROCDL/LowerGpuOpsToROCDLOps.cpp
Comment thread mlir/lib/Dialect/GPU/IR/GPUDialect.cpp Outdated
Comment thread mlir/lib/Dialect/GPU/Transforms/EliminateBarriers.cpp Outdated
@krzysz00
krzysz00 force-pushed the gpu-named-barrier branch from 81dada2 to bf035d9 Compare May 6, 2026 18:08
@krzysz00

krzysz00 commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

@grypp The current design doesn't require use-def analysis to get at the barrier ID - see the changes I pushed. We can allocate barrier IDs at lowering time, and then !gpu.named_barrier becomes an (i32 ID, i32 member count)`, which you can just pass around like any other value.

@krzysz00

krzysz00 commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

And half the point of this setup and of having an "initialize" setup is to, following the AMDGCN backend and SPIR-V, not require users to count

@grypp

grypp commented May 12, 2026

Copy link
Copy Markdown
Member

@grypp The current design doesn't require use-def analysis to get at the barrier ID - see the changes I pushed. We can allocate barrier IDs at lowering time, and then !gpu.named_barrier becomes an (i32 ID, i32 member count)`, which you can just pass around like any other value.

Ok nice. Can we also support attributes on this type so we don’t need to pass SSA values when calling a function?

// Today's behavior, all dynamic
!gpu.named_barrier<id=?, threads=?>

// Compile time known  
!gpu.named_barrier<id=4, threads=32>

@krzysz00

Copy link
Copy Markdown
Contributor Author

@grypp

Hm, yeah, not a bad idea - though I'm going to push back on threads and go with subgroups or members (I think it depends on what the semantics of cluster barriers will be in my neck of the woods)

On the other hand, Nvidia are the only people that are making the user count before hitting LLVM, so I'm tempted to go with the general abstraction. Is that something you'd be willing to take as a followup?

@fabianmcg
fabianmcg self-requested a review May 12, 2026 21:51

@fabianmcg fabianmcg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! Please wait a bit for any of the people that previously left comments.

@krzysz00
krzysz00 merged commit 755732f into llvm:main May 18, 2026
10 checks passed
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request May 19, 2026
…lvm#195692)

This commit adds two features to gpu.barrier that are supported on
targets like recent AMDGPU chips, Nvidia's hardware, and SPIR-V.

The first of these is named barriers, which allow creating a barrier
object that is initialized with the number of subgroups that must arrive
at it before those subgroups are released. These are represented in MLIR
with a new `!gpu.named_barrier` type and created by
`gpu.initialized_named_barrier` operation. These named barriers then
become arguments to `gpu.barrier`.

The other change is adding a "scope" enum and using it to specify the
execution scope of barriers. This allows for rerpresenting cluster- and
subgroup-wide barriers (the latter exists on AMDGPU and Nvidia, and
while I suspect Nvidia has cluster-scope barriers, I didn't go looking)
and allows us to fully lower to SPIR-V's OpControlBarrier.

While these are two different features, I figured I'd land them in one
PR so the full API for `gpu.barrier` only changes once.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants