[clang] [llvm] [SPIRV][HLSL] Fix Vulkan-invalid SPIR-V for Interlocked* on groupshared/UAV memory (PR #212663)

Joshua Batista via cfe-commits cfe-commits at lists.llvm.org
Tue Jul 28 18:04:45 PDT 2026


https://github.com/bob80905 updated https://github.com/llvm/llvm-project/pull/212663

>From 56404306415ddd3dcb9cb5cee44fd114d3fd72bc Mon Sep 17 00:00:00 2001
From: Joshua Batista <jbatista at microsoft.com>
Date: Tue, 28 Jul 2026 17:40:01 -0700
Subject: [PATCH 1/2] first attempt

---
 clang/lib/CodeGen/CGHLSLBuiltins.cpp          | 13 +++++++-
 .../CodeGenHLSL/builtins/InterlockedAdd.hlsl  | 24 +++++++-------
 .../CodeGenHLSL/builtins/InterlockedOr.hlsl   | 24 +++++++-------
 .../CodeGenHLSL/builtins/InterlockedXor.hlsl  | 26 ++++++++--------
 .../builtins/RWBuffer-Interlocked.hlsl        | 12 +++----
 .../RWByteAddressBuffer-InterlockedAdd.hlsl   | 16 +++++-----
 .../RWByteAddressBuffer-InterlockedOr.hlsl    | 16 +++++-----
 .../RWByteAddressBuffer-InterlockedXor.hlsl   | 16 +++++-----
 ...deredByteAddressBuffer-InterlockedAdd.hlsl |  8 ++---
 ...rderedByteAddressBuffer-InterlockedOr.hlsl |  8 ++---
 ...deredByteAddressBuffer-InterlockedXor.hlsl |  8 ++---
 llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp |  3 ++
 .../Target/SPIRV/SPIRVInstructionSelector.cpp | 14 ++++++++-
 llvm/lib/Target/SPIRV/SPIRVUtils.cpp          |  9 +++++-
 .../atomicrmw-vulkan-relaxed-workgroup.ll     | 26 ++++++++++++++++
 .../linkage/groupshared-no-import-linkage.ll  | 31 +++++++++++++++++++
 16 files changed, 172 insertions(+), 82 deletions(-)
 create mode 100644 llvm/test/CodeGen/SPIRV/atomicrmw-vulkan-relaxed-workgroup.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll

diff --git a/clang/lib/CodeGen/CGHLSLBuiltins.cpp b/clang/lib/CodeGen/CGHLSLBuiltins.cpp
index 0796d121a9b43..9818f94fcdaca 100644
--- a/clang/lib/CodeGen/CGHLSLBuiltins.cpp
+++ b/clang/lib/CodeGen/CGHLSLBuiltins.cpp
@@ -310,6 +310,16 @@ static Value *handleElementwiseF32ToF16(CodeGenFunction &CGF,
   llvm_unreachable("Intrinsic F32ToF16 not supported by target architecture");
 }
 
+// Pick the atomic memory scope for an HLSL Interlocked* op by destination:
+// Workgroup for groupshared, Device otherwise (Vulkan forbids CrossDevice).
+static llvm::SyncScope::ID GetInterlockedScope(CodeGenFunction &CGF,
+                                               const LValue &DestLV) {
+  StringRef ScopeName = DestLV.getAddressSpace() == LangAS::hlsl_groupshared
+                            ? "workgroup"
+                            : "device";
+  return CGF.getLLVMContext().getOrInsertSyncScopeID(ScopeName);
+}
+
 static Value *handleInterlockedOp(CodeGenFunction &CGF, const CallExpr *E,
                                   llvm::AtomicRMWInst::BinOp Op) {
   // Emit `atomicrmw <op>` directly — no intermediate intrinsic needed on
@@ -321,7 +331,8 @@ static Value *handleInterlockedOp(CodeGenFunction &CGF, const CallExpr *E,
          "Intrinsic InterlockedOp value operand must be an integer");
 
   llvm::AtomicRMWInst *Call = CGF.Builder.CreateAtomicRMW(
-      Op, DestAddr, Val, llvm::AtomicOrdering::Monotonic);
+      Op, DestAddr, Val, llvm::AtomicOrdering::Monotonic,
+      GetInterlockedScope(CGF, DestLV));
 
   // The 3-arg overload writes the old value (the RMW's return value) into
   // the `original_value` reference parameter.
diff --git a/clang/test/CodeGenHLSL/builtins/InterlockedAdd.hlsl b/clang/test/CodeGenHLSL/builtins/InterlockedAdd.hlsl
index 6bb4fd25c4a0c..a878c372dcea6 100644
--- a/clang/test/CodeGenHLSL/builtins/InterlockedAdd.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/InterlockedAdd.hlsl
@@ -14,45 +14,45 @@ groupshared int64_t  gs_i64;
 groupshared uint64_t gs_u64;
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int_2arg
-// DXCHECK:  atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 export void test_int_2arg(int v) {
   InterlockedAdd(gs_i32, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint_2arg
-// DXCHECK:  atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 export void test_uint_2arg(uint v) {
   InterlockedAdd(gs_u32, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i32 %[[R]], ptr {{.*}}
 export void test_int_3arg(int v, out int orig) {
   InterlockedAdd(gs_i32, v, orig);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i32 %[[R]], ptr {{.*}}
 export void test_uint_3arg(uint v, out uint orig) {
   InterlockedAdd(gs_u32, v, orig);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int64_2arg
-// DXCHECK:  atomicrmw add ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} monotonic
-// SPVCHECK: atomicrmw add ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} monotonic
+// DXCHECK:  atomicrmw add ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw add ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
 export void test_int64_2arg(int64_t v) {
   InterlockedAdd(gs_i64, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint64_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i64 %[[R]], ptr {{.*}}
 export void test_uint64_3arg(uint64_t v, out uint64_t orig) {
   InterlockedAdd(gs_u64, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/InterlockedOr.hlsl b/clang/test/CodeGenHLSL/builtins/InterlockedOr.hlsl
index 9c23c5a055a03..c5def7368ecc6 100644
--- a/clang/test/CodeGenHLSL/builtins/InterlockedOr.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/InterlockedOr.hlsl
@@ -14,45 +14,45 @@ groupshared int64_t  gs_i64;
 groupshared uint64_t gs_u64;
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int_2arg
-// DXCHECK:  atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 export void test_int_2arg(int v) {
   InterlockedOr(gs_i32, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint_2arg
-// DXCHECK:  atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 export void test_uint_2arg(uint v) {
   InterlockedOr(gs_u32, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i32 %[[R]], ptr {{.*}}
 export void test_int_3arg(int v, out int orig) {
   InterlockedOr(gs_i32, v, orig);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i32 %[[R]], ptr {{.*}}
 export void test_uint_3arg(uint v, out uint orig) {
   InterlockedOr(gs_u32, v, orig);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int64_2arg
-// DXCHECK:  atomicrmw or ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} monotonic
-// SPVCHECK: atomicrmw or ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} monotonic
+// DXCHECK:  atomicrmw or ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw or ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
 export void test_int64_2arg(int64_t v) {
   InterlockedOr(gs_i64, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint64_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i64 %[[R]], ptr {{.*}}
 export void test_uint64_3arg(uint64_t v, out uint64_t orig) {
   InterlockedOr(gs_u64, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/InterlockedXor.hlsl b/clang/test/CodeGenHLSL/builtins/InterlockedXor.hlsl
index 8cd799c09eb01..e0a60736980ec 100644
--- a/clang/test/CodeGenHLSL/builtins/InterlockedXor.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/InterlockedXor.hlsl
@@ -6,7 +6,7 @@
 // RUN:   spirv-pc-vulkan-library %s -emit-llvm -disable-llvm-passes -o - | \
 // RUN:   FileCheck %s --check-prefixes=CHECK,SPVCHECK
 
-// Test basic lowering of HLSL InterlockedXor to `atomicrmw or monotonic`.
+// Test basic lowering of HLSL InterlockedXor to `atomicrmw xor monotonic`.
 
 groupshared int  gs_i32;
 groupshared uint gs_u32;
@@ -14,45 +14,45 @@ groupshared int64_t  gs_i64;
 groupshared uint64_t gs_u64;
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int_2arg
-// DXCHECK:  atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 export void test_int_2arg(int v) {
   InterlockedXor(gs_i32, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint_2arg
-// DXCHECK:  atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 export void test_uint_2arg(uint v) {
   InterlockedXor(gs_u32, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_i32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i32 %[[R]], ptr {{.*}}
 export void test_int_3arg(int v, out int orig) {
   InterlockedXor(gs_i32, v, orig);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u32{{.*}}, i32 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i32 %[[R]], ptr {{.*}}
 export void test_uint_3arg(uint v, out uint orig) {
   InterlockedXor(gs_u32, v, orig);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_int64_2arg
-// DXCHECK:  atomicrmw xor ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} monotonic
-// SPVCHECK: atomicrmw xor ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} monotonic
+// DXCHECK:  atomicrmw xor ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: atomicrmw xor ptr addrspace(3) {{.*}}@gs_i64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
 export void test_int64_2arg(int64_t v) {
   InterlockedXor(gs_i64, v);
 }
 
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_uint64_3arg
-// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} monotonic
-// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(3) {{.*}}@gs_u64{{.*}}, i64 %{{.*}} syncscope("workgroup") monotonic
 // CHECK:    store i64 %[[R]], ptr {{.*}}
 export void test_uint64_3arg(uint64_t v, out uint64_t orig) {
   InterlockedXor(gs_u64, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/RWBuffer-Interlocked.hlsl b/clang/test/CodeGenHLSL/builtins/RWBuffer-Interlocked.hlsl
index 4d7050ba3a43c..0ef2b86bc45a5 100644
--- a/clang/test/CodeGenHLSL/builtins/RWBuffer-Interlocked.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/RWBuffer-Interlocked.hlsl
@@ -18,17 +18,17 @@ RWBuffer<int> Out : register(u0);
 
 // CHECK-LABEL: define void @main
 // DXCHECK:  %[[PTR1:.*]] = call {{.*}} @llvm.dx.resource.getpointer.p0.tdx.TypedBuffer_i32_1_0_1t.i32(target("dx.TypedBuffer", i32, 1, 0, 1) %{{.*}}, i32 %{{.*}})
-// DXCHECK:  atomicrmw add ptr %[[PTR1]], i32 1 monotonic
+// DXCHECK:  atomicrmw add ptr %[[PTR1]], i32 1 syncscope("device") monotonic
 // DXCHECK:  %[[PTR2:.*]] = call {{.*}} @llvm.dx.resource.getpointer.p0.tdx.TypedBuffer_i32_1_0_1t.i32(target("dx.TypedBuffer", i32, 1, 0, 1) %{{.*}}, i32 %{{.*}})
-// DXCHECK:  atomicrmw or ptr %[[PTR2]], i32 1 monotonic
+// DXCHECK:  atomicrmw or ptr %[[PTR2]], i32 1 syncscope("device") monotonic
 // DXCHECK:  %[[PTR3:.*]] = call {{.*}} @llvm.dx.resource.getpointer.p0.tdx.TypedBuffer_i32_1_0_1t.i32(target("dx.TypedBuffer", i32, 1, 0, 1) %{{.*}}, i32 %{{.*}})
-// DXCHECK:  atomicrmw xor ptr %[[PTR3]], i32 1 monotonic
+// DXCHECK:  atomicrmw xor ptr %[[PTR3]], i32 1 syncscope("device") monotonic
 // SPVCHECK: %[[PTR1:.*]] = call {{.*}} @llvm.spv.resource.getpointer.{{.*}}(target("spirv.{{Image|SignedImage}}", i32, {{.*}}) %{{.*}}, i32 %{{.*}})
-// SPVCHECK: atomicrmw add ptr addrspace(11) %[[PTR1]], i32 1 monotonic
+// SPVCHECK: atomicrmw add ptr addrspace(11) %[[PTR1]], i32 1 syncscope("device") monotonic
 // SPVCHECK: %[[PTR2:.*]] = call {{.*}} @llvm.spv.resource.getpointer.{{.*}}(target("spirv.{{Image|SignedImage}}", i32, {{.*}}) %{{.*}}, i32 %{{.*}})
-// SPVCHECK: atomicrmw or ptr addrspace(11) %[[PTR2]], i32 1 monotonic
+// SPVCHECK: atomicrmw or ptr addrspace(11) %[[PTR2]], i32 1 syncscope("device") monotonic
 // SPVCHECK: %[[PTR3:.*]] = call {{.*}} @llvm.spv.resource.getpointer.{{.*}}(target("spirv.{{Image|SignedImage}}", i32, {{.*}}) %{{.*}}, i32 %{{.*}})
-// SPVCHECK: atomicrmw xor ptr addrspace(11) %[[PTR3]], i32 1 monotonic
+// SPVCHECK: atomicrmw xor ptr addrspace(11) %[[PTR3]], i32 1 syncscope("device") monotonic
 [shader("compute")]
 [numthreads(1,1,1)]
 void main(uint3 id : SV_DispatchThreadID) {
diff --git a/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedAdd.hlsl b/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedAdd.hlsl
index 576a1f0b70b95..5342c0a7c93f7 100644
--- a/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedAdd.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedAdd.hlsl
@@ -16,10 +16,10 @@ RWByteAddressBuffer BAB : register(u0);
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_int_2arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  atomicrmw add ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw add ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: atomicrmw add ptr addrspace(11) %[[PTR]], i32 %{{.*}} monotonic
+// SPVCHECK: atomicrmw add ptr addrspace(11) %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 export void test_bab_int_2arg(uint off, int v) {
   BAB.InterlockedAdd(off, v);
 }
@@ -27,11 +27,11 @@ export void test_bab_int_2arg(uint off, int v) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_uint_3arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  %[[R:.*]] = atomicrmw add ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw add ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // DXCHECK:  store i32 %[[R]], ptr {{.*}}
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(11) %[[PTR]], i32 %{{.*}} monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(11) %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: store i32 %[[R]], ptr {{.*}}
 export void test_bab_uint_3arg(uint off, uint v, out uint orig) {
   BAB.InterlockedAdd(off, v, orig);
@@ -40,10 +40,10 @@ export void test_bab_uint_3arg(uint off, uint v, out uint orig) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_int64_2arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  atomicrmw add ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK:  atomicrmw add ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: atomicrmw add ptr addrspace(11) %[[PTR]], i64 %{{.*}} monotonic
+// SPVCHECK: atomicrmw add ptr addrspace(11) %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 export void test_bab_int64_2arg(uint off, int64_t v) {
   BAB.InterlockedAdd64(off, v);
 }
@@ -51,11 +51,11 @@ export void test_bab_int64_2arg(uint off, int64_t v) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_uint64_3arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  %[[R:.*]] = atomicrmw add ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw add ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // DXCHECK:  store i64 %[[R]], ptr {{.*}}
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(11) %[[PTR]], i64 %{{.*}} monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw add ptr addrspace(11) %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: store i64 %[[R]], ptr {{.*}}
 export void test_bab_uint64_3arg(uint off, uint64_t v, out uint64_t orig) {
   BAB.InterlockedAdd64(off, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedOr.hlsl b/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedOr.hlsl
index 874474d043d5e..b9c6bbb4dcdd5 100644
--- a/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedOr.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedOr.hlsl
@@ -16,10 +16,10 @@ RWByteAddressBuffer BAB : register(u0);
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_int_2arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  atomicrmw or ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw or ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: atomicrmw or ptr addrspace(11) %[[PTR]], i32 %{{.*}} monotonic
+// SPVCHECK: atomicrmw or ptr addrspace(11) %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 export void test_bab_int_2arg(uint off, int v) {
   BAB.InterlockedOr(off, v);
 }
@@ -27,11 +27,11 @@ export void test_bab_int_2arg(uint off, int v) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_uint_3arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  %[[R:.*]] = atomicrmw or ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw or ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // DXCHECK:  store i32 %[[R]], ptr {{.*}}
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(11) %[[PTR]], i32 %{{.*}} monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(11) %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: store i32 %[[R]], ptr {{.*}}
 export void test_bab_uint_3arg(uint off, uint v, out uint orig) {
   BAB.InterlockedOr(off, v, orig);
@@ -40,10 +40,10 @@ export void test_bab_uint_3arg(uint off, uint v, out uint orig) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_int64_2arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  atomicrmw or ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK:  atomicrmw or ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: atomicrmw or ptr addrspace(11) %[[PTR]], i64 %{{.*}} monotonic
+// SPVCHECK: atomicrmw or ptr addrspace(11) %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 export void test_bab_int64_2arg(uint off, int64_t v) {
   BAB.InterlockedOr64(off, v);
 }
@@ -51,11 +51,11 @@ export void test_bab_int64_2arg(uint off, int64_t v) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_uint64_3arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  %[[R:.*]] = atomicrmw or ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw or ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // DXCHECK:  store i64 %[[R]], ptr {{.*}}
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(11) %[[PTR]], i64 %{{.*}} monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw or ptr addrspace(11) %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: store i64 %[[R]], ptr {{.*}}
 export void test_bab_uint64_3arg(uint off, uint64_t v, out uint64_t orig) {
   BAB.InterlockedOr64(off, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedXor.hlsl b/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedXor.hlsl
index f4f788b7968a5..41cd077442d8e 100644
--- a/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedXor.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/RWByteAddressBuffer-InterlockedXor.hlsl
@@ -16,10 +16,10 @@ RWByteAddressBuffer BAB : register(u0);
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_int_2arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  atomicrmw xor ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK:  atomicrmw xor ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: atomicrmw xor ptr addrspace(11) %[[PTR]], i32 %{{.*}} monotonic
+// SPVCHECK: atomicrmw xor ptr addrspace(11) %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 export void test_bab_int_2arg(uint off, int v) {
   BAB.InterlockedXor(off, v);
 }
@@ -27,11 +27,11 @@ export void test_bab_int_2arg(uint off, int v) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_uint_3arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // DXCHECK:  store i32 %[[R]], ptr {{.*}}
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(11) %[[PTR]], i32 %{{.*}} monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(11) %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: store i32 %[[R]], ptr {{.*}}
 export void test_bab_uint_3arg(uint off, uint v, out uint orig) {
   BAB.InterlockedXor(off, v, orig);
@@ -40,10 +40,10 @@ export void test_bab_uint_3arg(uint off, uint v, out uint orig) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_int64_2arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  atomicrmw xor ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK:  atomicrmw xor ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: atomicrmw xor ptr addrspace(11) %[[PTR]], i64 %{{.*}} monotonic
+// SPVCHECK: atomicrmw xor ptr addrspace(11) %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 export void test_bab_int64_2arg(uint off, int64_t v) {
   BAB.InterlockedXor64(off, v);
 }
@@ -51,11 +51,11 @@ export void test_bab_int64_2arg(uint off, int64_t v) {
 // CHECK-LABEL: define {{(dso_local |hidden |internal |protected |spir_func )*}}void @{{.*}}test_bab_uint64_3arg
 // DXCHECK:  %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 0), ptr {{.*}}
 // DXCHECK:  %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_0t.i32(target("dx.RawBuffer", i8, 1, 0) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK:  %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // DXCHECK:  store i64 %[[R]], ptr {{.*}}
 // SPVCHECK: %[[HANDLE:.*]] = load target("spirv.VulkanBuffer", [0 x i8], 12, 1), ptr {{.*}}
 // SPVCHECK: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0i8_12_1t.i32(target("spirv.VulkanBuffer", [0 x i8], 12, 1) %[[HANDLE]], i32 %{{.*}})
-// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(11) %[[PTR]], i64 %{{.*}} monotonic
+// SPVCHECK: %[[R:.*]] = atomicrmw xor ptr addrspace(11) %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // SPVCHECK: store i64 %[[R]], ptr {{.*}}
 export void test_bab_uint64_3arg(uint off, uint64_t v, out uint64_t orig) {
   BAB.InterlockedXor64(off, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedAdd.hlsl b/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedAdd.hlsl
index 377269ac9beda..0b910728232dd 100644
--- a/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedAdd.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedAdd.hlsl
@@ -13,7 +13,7 @@ RasterizerOrderedByteAddressBuffer ROVB : register(u1);
 // CHECK-LABEL: define void @{{.*}}test_rovb_int_2arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: atomicrmw add ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK: atomicrmw add ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 export void test_rovb_int_2arg(uint off, int v) {
   ROVB.InterlockedAdd(off, v);
 }
@@ -21,7 +21,7 @@ export void test_rovb_int_2arg(uint off, int v) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_uint_3arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: %[[R:.*]] = atomicrmw add ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK: %[[R:.*]] = atomicrmw add ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // DXCHECK: store i32 %[[R]], ptr {{.*}}
 export void test_rovb_uint_3arg(uint off, uint v, out uint orig) {
   ROVB.InterlockedAdd(off, v, orig);
@@ -30,7 +30,7 @@ export void test_rovb_uint_3arg(uint off, uint v, out uint orig) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_int64_2arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: atomicrmw add ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK: atomicrmw add ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 export void test_rovb_int64_2arg(uint off, int64_t v) {
   ROVB.InterlockedAdd64(off, v);
 }
@@ -38,7 +38,7 @@ export void test_rovb_int64_2arg(uint off, int64_t v) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_uint64_3arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: %[[R:.*]] = atomicrmw add ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK: %[[R:.*]] = atomicrmw add ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // DXCHECK: store i64 %[[R]], ptr {{.*}}
 export void test_rovb_uint64_3arg(uint off, uint64_t v, out uint64_t orig) {
   ROVB.InterlockedAdd64(off, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedOr.hlsl b/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedOr.hlsl
index 4c8f48880bdf7..81991662a014d 100644
--- a/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedOr.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedOr.hlsl
@@ -13,7 +13,7 @@ RasterizerOrderedByteAddressBuffer ROVB : register(u1);
 // CHECK-LABEL: define void @{{.*}}test_rovb_int_2arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: atomicrmw or ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK: atomicrmw or ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 export void test_rovb_int_2arg(uint off, int v) {
   ROVB.InterlockedOr(off, v);
 }
@@ -21,7 +21,7 @@ export void test_rovb_int_2arg(uint off, int v) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_uint_3arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: %[[R:.*]] = atomicrmw or ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK: %[[R:.*]] = atomicrmw or ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // DXCHECK: store i32 %[[R]], ptr {{.*}}
 export void test_rovb_uint_3arg(uint off, uint v, out uint orig) {
   ROVB.InterlockedOr(off, v, orig);
@@ -30,7 +30,7 @@ export void test_rovb_uint_3arg(uint off, uint v, out uint orig) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_int64_2arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: atomicrmw or ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK: atomicrmw or ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 export void test_rovb_int64_2arg(uint off, int64_t v) {
   ROVB.InterlockedOr64(off, v);
 }
@@ -38,7 +38,7 @@ export void test_rovb_int64_2arg(uint off, int64_t v) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_uint64_3arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: %[[R:.*]] = atomicrmw or ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK: %[[R:.*]] = atomicrmw or ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // DXCHECK: store i64 %[[R]], ptr {{.*}}
 export void test_rovb_uint64_3arg(uint off, uint64_t v, out uint64_t orig) {
   ROVB.InterlockedOr64(off, v, orig);
diff --git a/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedXor.hlsl b/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedXor.hlsl
index b8dfa4f81ee73..4433331ea49ea 100644
--- a/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedXor.hlsl
+++ b/clang/test/CodeGenHLSL/builtins/RasterizerOrderedByteAddressBuffer-InterlockedXor.hlsl
@@ -13,7 +13,7 @@ RasterizerOrderedByteAddressBuffer ROVB : register(u1);
 // CHECK-LABEL: define void @{{.*}}test_rovb_int_2arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: atomicrmw xor ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK: atomicrmw xor ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 export void test_rovb_int_2arg(uint off, int v) {
   ROVB.InterlockedXor(off, v);
 }
@@ -21,7 +21,7 @@ export void test_rovb_int_2arg(uint off, int v) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_uint_3arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i32 %{{.*}} monotonic
+// DXCHECK: %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i32 %{{.*}} syncscope("device") monotonic
 // DXCHECK: store i32 %[[R]], ptr {{.*}}
 export void test_rovb_uint_3arg(uint off, uint v, out uint orig) {
   ROVB.InterlockedXor(off, v, orig);
@@ -30,7 +30,7 @@ export void test_rovb_uint_3arg(uint off, uint v, out uint orig) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_int64_2arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: atomicrmw xor ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK: atomicrmw xor ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 export void test_rovb_int64_2arg(uint off, int64_t v) {
   ROVB.InterlockedXor64(off, v);
 }
@@ -38,7 +38,7 @@ export void test_rovb_int64_2arg(uint off, int64_t v) {
 // CHECK-LABEL: define void @{{.*}}test_rovb_uint64_3arg
 // DXCHECK: %[[HANDLE:.*]] = load target("dx.RawBuffer", i8, 1, 1), ptr {{.*}}
 // DXCHECK: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.tdx.RawBuffer_i8_1_1t.i32(target("dx.RawBuffer", i8, 1, 1) %[[HANDLE]], i32 %{{.*}})
-// DXCHECK: %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i64 %{{.*}} monotonic
+// DXCHECK: %[[R:.*]] = atomicrmw xor ptr %[[PTR]], i64 %{{.*}} syncscope("device") monotonic
 // DXCHECK: store i64 %[[R]], ptr {{.*}}
 export void test_rovb_uint64_3arg(uint off, uint64_t v, out uint64_t orig) {
   ROVB.InterlockedXor64(off, v, orig);
diff --git a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
index 513b3149dd28f..921d668558068 100644
--- a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
@@ -2599,6 +2599,9 @@ SPIRVEmitIntrinsicsImpl::visitAtomicCmpXchgInst(AtomicCmpXchgInst &I) {
   unsigned AS = I.getPointerOperand()->getType()->getPointerAddressSpace();
   uint32_t ScSem = static_cast<uint32_t>(
       getMemSemanticsForStorageClass(addressSpaceToStorageClass(AS, ST)));
+  // TODO: Like selectAtomicRMW, a relaxed shader atomic must not carry a
+  // storage-class bit (Vulkan rejects it). Drop ScSem for the success/failure
+  // semantics when the order is None and ST.isShader() once HLSL emits cmpxchg.
   Args.push_back(B.getInt32(
       static_cast<uint32_t>(getMemSemantics(I.getSuccessOrdering())) | ScSem));
   Args.push_back(B.getInt32(
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
index c8f705e0251b6..e9eebc93ec00d 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
@@ -2102,6 +2102,9 @@ bool SPIRVInstructionSelector::selectAtomicLoad(Register ResVReg,
   uint32_t MemSem = static_cast<uint32_t>(getMemSemantics(AO));
   if (MemOp.isVolatile() && STI.getTargetTriple().isVulkanOS())
     MemSem |= static_cast<uint32_t>(SPIRV::MemorySemantics::Volatile);
+  // TODO: Like selectAtomicRMW, a relaxed shader atomic must not carry a
+  // storage-class bit (Vulkan rejects it). Drop StorageClass here when
+  // MemSem is None and STI.isShader() once such an atomic load is reachable.
   Register MemSemReg = buildI32Constant(MemSem | StorageClass, I);
 
   MachineIRBuilder MIRBuilder(I);
@@ -2251,6 +2254,9 @@ bool SPIRVInstructionSelector::selectAtomicStore(MachineInstr &I) const {
   uint32_t MemSem = static_cast<uint32_t>(getMemSemantics(AO));
   if (MemOp.isVolatile() && STI.getTargetTriple().isVulkanOS())
     MemSem |= static_cast<uint32_t>(SPIRV::MemorySemantics::Volatile);
+  // TODO: Like selectAtomicRMW, a relaxed shader atomic must not carry a
+  // storage-class bit (Vulkan rejects it). Drop StorageClass here when
+  // MemSem is None and STI.isShader() once such an atomic store is reachable.
   Register MemSemReg = buildI32Constant(MemSem | StorageClass, I);
   MachineIRBuilder MIRBuilder(I);
 
@@ -2524,7 +2530,13 @@ bool SPIRVInstructionSelector::selectAtomicRMW(Register ResVReg,
   uint32_t ScSem = static_cast<uint32_t>(
       getMemSemanticsForStorageClass(GR.getPointerStorageClass(Ptr)));
   AtomicOrdering AO = MemOp->getSuccessOrdering();
-  uint32_t MemSem = static_cast<uint32_t>(getMemSemantics(AO)) | ScSem;
+  uint32_t OrderSem = static_cast<uint32_t>(getMemSemantics(AO));
+  // Vulkan forbids a storage-class semantics bit (e.g. WorkgroupMemory) with a
+  // relaxed order, so a relaxed shader atomic must carry no storage-class bit.
+  if (STI.isShader() &&
+      OrderSem == static_cast<uint32_t>(SPIRV::MemorySemantics::None))
+    ScSem = 0;
+  uint32_t MemSem = OrderSem | ScSem;
   Register MemSemReg = buildI32Constant(MemSem, I);
 
   Register ValueReg = I.getOperand(2).getReg();
diff --git a/llvm/lib/Target/SPIRV/SPIRVUtils.cpp b/llvm/lib/Target/SPIRV/SPIRVUtils.cpp
index 236da61192f1a..825bc147f92f5 100644
--- a/llvm/lib/Target/SPIRV/SPIRVUtils.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVUtils.cpp
@@ -1254,13 +1254,20 @@ getSpirvLinkageTypeFor(const SPIRVSubtarget &ST, const GlobalValue &GV) {
     return std::nullopt;
 
   if (GV.isDeclarationForLinker()) {
-    // Interface variables must not get Import linkage.
     if (const auto *GVar = dyn_cast<GlobalVariable>(&GV)) {
       auto SC = addressSpaceToStorageClass(GVar->getAddressSpace(), ST);
+      // Interface variables (shader-only storage classes) must never get
+      // Import linkage.
       if (SC == SPIRV::StorageClass::Input ||
           SC == SPIRV::StorageClass::Output ||
           SC == SPIRV::StorageClass::PushConstant)
         return std::nullopt;
+      // Shaders have no linker, so module-internal storage (e.g. HLSL
+      // groupshared) can't be imported; kernels (e.g. HIP LDS) legitimately
+      // can.
+      if (ST.isShader() && (SC == SPIRV::StorageClass::Workgroup ||
+                            SC == SPIRV::StorageClass::Private))
+        return std::nullopt;
     }
     return SPIRV::LinkageType::Import;
   }
diff --git a/llvm/test/CodeGen/SPIRV/atomicrmw-vulkan-relaxed-workgroup.ll b/llvm/test/CodeGen/SPIRV/atomicrmw-vulkan-relaxed-workgroup.ll
new file mode 100644
index 0000000000000..b90175ca5b300
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/atomicrmw-vulkan-relaxed-workgroup.ll
@@ -0,0 +1,26 @@
+; In a Vulkan (shader) environment a relaxed (monotonic) atomic must NOT set a
+; storage-class memory-semantics bit (e.g. WorkgroupMemory): Vulkan requires
+; such bits to be paired with a non-relaxed order (Acquire/Release/AcqRel), so
+; pairing WorkgroupMemory with a relaxed order is rejected by spirv-val
+; (VUID-StandaloneSpirv-MemorySemantics-10871). This is the shape emitted for
+; HLSL Interlocked* on `groupshared` memory, which uses a "workgroup" syncscope
+; and a relaxed order. The scope must still be Workgroup and the semantics None.
+
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
+
+; CHECK-DAG: %[[#U32:]] = OpTypeInt 32 0
+; Workgroup scope = 2, None (relaxed) memory semantics = 0.
+; CHECK-DAG: %[[#WG:]] = OpConstant %[[#U32]] 2
+; CHECK-DAG: %[[#NONE:]] = OpConstantNull %[[#U32]]
+
+ at gs = external hidden addrspace(3) global i32, align 4
+
+define void @main() #0 {
+entry:
+  ; CHECK: OpAtomicOr %[[#U32]] %{{[0-9]+}} %[[#WG]] %[[#NONE]] %{{[0-9]+}}
+  %0 = atomicrmw or ptr addrspace(3) @gs, i32 1 syncscope("workgroup") monotonic
+  ret void
+}
+
+attributes #0 = { "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" }
diff --git a/llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll b/llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll
new file mode 100644
index 0000000000000..9131843ff5d0b
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll
@@ -0,0 +1,31 @@
+; HLSL `groupshared` variables are emitted by clang as external, hidden globals
+; in the Workgroup storage class (a declaration, since shared memory has no
+; initializer). getSpirvLinkageTypeFor must NOT give such module-internal
+; storage-class declarations Import linkage: doing so decorates them with
+; LinkageAttributes and forces OpCapability Linkage, which is illegal in a
+; Vulkan shader. Verify no Linkage capability / decoration is emitted while the
+; variable is still materialized as a Workgroup OpVariable.
+
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - | FileCheck %s --check-prefixes=CHECK,NOLINK
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
+
+; The groupshared variable is still emitted, in the Workgroup storage class.
+; CHECK-DAG: OpName %[[#gs:]] "gs"
+; CHECK-DAG: %[[#PtrWG:]] = OpTypePointer Workgroup %{{[0-9]+}}
+; CHECK-DAG: %[[#gs]] = OpVariable %[[#PtrWG]] Workgroup
+
+; No Linkage capability and no LinkageAttributes decoration may be present.
+; A NOT-only prefix scans the whole module (the capability and decoration
+; sections precede the OpVariable definition in SPIR-V layout).
+; NOLINK-NOT: OpCapability Linkage
+; NOLINK-NOT: LinkageAttributes
+
+ at gs = external hidden addrspace(3) global i32, align 4
+
+define void @main() #0 {
+entry:
+  store i32 0, ptr addrspace(3) @gs, align 4
+  ret void
+}
+
+attributes #0 = { "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" }

>From f02d2111ea632db6cb125c51f32a568eee571fdf Mon Sep 17 00:00:00 2001
From: Joshua Batista <jbatista at microsoft.com>
Date: Tue, 28 Jul 2026 18:04:32 -0700
Subject: [PATCH 2/2] self review

---
 llvm/lib/Target/SPIRV/SPIRVUtils.cpp                      | 8 +++-----
 .../SPIRV/linkage/groupshared-no-import-linkage.ll        | 6 +++---
 2 files changed, 6 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVUtils.cpp b/llvm/lib/Target/SPIRV/SPIRVUtils.cpp
index 825bc147f92f5..5e416374dd829 100644
--- a/llvm/lib/Target/SPIRV/SPIRVUtils.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVUtils.cpp
@@ -1256,15 +1256,13 @@ getSpirvLinkageTypeFor(const SPIRVSubtarget &ST, const GlobalValue &GV) {
   if (GV.isDeclarationForLinker()) {
     if (const auto *GVar = dyn_cast<GlobalVariable>(&GV)) {
       auto SC = addressSpaceToStorageClass(GVar->getAddressSpace(), ST);
-      // Interface variables (shader-only storage classes) must never get
-      // Import linkage.
+      // Interface variables must not get Import linkage.
       if (SC == SPIRV::StorageClass::Input ||
           SC == SPIRV::StorageClass::Output ||
           SC == SPIRV::StorageClass::PushConstant)
         return std::nullopt;
-      // Shaders have no linker, so module-internal storage (e.g. HLSL
-      // groupshared) can't be imported; kernels (e.g. HIP LDS) legitimately
-      // can.
+      // Shaders have no linker, so module-internal storage
+      // (e.g. HLSL groupshared) can't be imported
       if (ST.isShader() && (SC == SPIRV::StorageClass::Workgroup ||
                             SC == SPIRV::StorageClass::Private))
         return std::nullopt;
diff --git a/llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll b/llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll
index 9131843ff5d0b..8d8c81950bfc1 100644
--- a/llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll
+++ b/llvm/test/CodeGen/SPIRV/linkage/groupshared-no-import-linkage.ll
@@ -6,7 +6,7 @@
 ; Vulkan shader. Verify no Linkage capability / decoration is emitted while the
 ; variable is still materialized as a Workgroup OpVariable.
 
-; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - | FileCheck %s --check-prefixes=CHECK,NOLINK
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - | FileCheck %s
 ; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
 
 ; The groupshared variable is still emitted, in the Workgroup storage class.
@@ -17,8 +17,8 @@
 ; No Linkage capability and no LinkageAttributes decoration may be present.
 ; A NOT-only prefix scans the whole module (the capability and decoration
 ; sections precede the OpVariable definition in SPIR-V layout).
-; NOLINK-NOT: OpCapability Linkage
-; NOLINK-NOT: LinkageAttributes
+; CHECK-NOT: OpCapability Linkage
+; CHECK-NOT: LinkageAttributes
 
 @gs = external hidden addrspace(3) global i32, align 4
 



More information about the cfe-commits mailing list