Skip to content

[SPIRV] Add support for SPV_KHR_abort extension#193037

Merged
michalpaszkowski merged 11 commits into
llvm:mainfrom
vmustya:spv-khr-abort
May 6, 2026
Merged

[SPIRV] Add support for SPV_KHR_abort extension#193037
michalpaszkowski merged 11 commits into
llvm:mainfrom
vmustya:spv-khr-abort

Conversation

@vmustya

@vmustya vmustya commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

This commit adds support for the SPV_KHR_abort extension in the SPIRV
backend. The extension allows shaders to abort execution with a custom
message.

Assisted-by: Claude Opus 4.7 noreply@anthropic.com

This commit adds support for the SPV_KHR_abort extension in the SPIRV
backend. The extension allows shaders to abort execution with a custom
message.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>
@llvmbot

llvmbot commented Apr 20, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-backend-spir-v

@llvm/pr-subscribers-llvm-ir

Author: Victor Mustya (vmustya)

Changes

This commit adds support for the SPV_KHR_abort extension in the SPIRV
backend. The extension allows shaders to abort execution with a custom
message.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>


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

16 Files Affected:

  • (modified) llvm/docs/SPIRVUsage.rst (+2)
  • (modified) llvm/include/llvm/IR/IntrinsicsSPIRV.td (+1)
  • (modified) llvm/lib/Target/SPIRV/SPIRVCommandLine.cpp (+2-1)
  • (modified) llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp (+14)
  • (modified) llvm/lib/Target/SPIRV/SPIRVInstrInfo.td (+2)
  • (modified) llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp (+61)
  • (modified) llvm/lib/Target/SPIRV/SPIRVModuleAnalysis.cpp (+8)
  • (modified) llvm/lib/Target/SPIRV/SPIRVPrepareFunctions.cpp (+60)
  • (modified) llvm/lib/Target/SPIRV/SPIRVSymbolicOperands.td (+5)
  • (added) llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite-construct.ll (+30)
  • (added) llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite.ll (+51)
  • (added) llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-invalid-message-type.ll (+61)
  • (added) llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-opencl-cfg.ll (+40)
  • (added) llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-opencl-source.ll (+81)
  • (added) llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-opencl.ll (+62)
  • (added) llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort.ll (+24)
diff --git a/llvm/docs/SPIRVUsage.rst b/llvm/docs/SPIRVUsage.rst
index 75a477b86da05..e66f8bb374a94 100644
--- a/llvm/docs/SPIRVUsage.rst
+++ b/llvm/docs/SPIRVUsage.rst
@@ -213,6 +213,8 @@ Below is a list of supported SPIR-V extensions, sorted alphabetically by their e
      - Allows to allocate local arrays whose number of elements is unknown at compile time.
    * - ``SPV_INTEL_joint_matrix``
      - Adds few matrix capabilities on top of SPV_KHR_cooperative_matrix extension, such as matrix prefetch, get element coordinate and checked load/store/construct instructions, tensor float 32 and bfloat type interpretations for multiply-add instruction.
+   * - ``SPV_KHR_abort``
+     - Adds a function-termination instruction that signals an abnormal error to the client API. Currently supported in OpenCL environments only.
    * - ``SPV_KHR_bit_instructions``
      - Enables bit instructions to be used by SPIR-V modules without requiring the Shader capability.
    * - ``SPV_KHR_expect_assume``
diff --git a/llvm/include/llvm/IR/IntrinsicsSPIRV.td b/llvm/include/llvm/IR/IntrinsicsSPIRV.td
index d2a5fa1f08724..c4733d3942f2e 100644
--- a/llvm/include/llvm/IR/IntrinsicsSPIRV.td
+++ b/llvm/include/llvm/IR/IntrinsicsSPIRV.td
@@ -39,6 +39,7 @@ let TargetPrefix = "spv" in {
   def int_spv_selection_merge : Intrinsic<[], [llvm_any_ty, llvm_i32_ty], [ImmArg<ArgIndex<1>>]>;
   def int_spv_cmpxchg : Intrinsic<[llvm_i32_ty], [llvm_any_ty, llvm_vararg_ty]>;
   def int_spv_unreachable : Intrinsic<[], []>;
+  def int_spv_abort : Intrinsic<[], [llvm_vararg_ty], [IntrNoReturn]>;
   def int_spv_alloca : Intrinsic<[llvm_any_ty], [llvm_i32_ty], [ImmArg<ArgIndex<0>>]>;
   def int_spv_alloca_array : Intrinsic<[llvm_any_ty], [llvm_anyint_ty, llvm_i32_ty], [ImmArg<ArgIndex<1>>]>;
   def int_spv_undef : Intrinsic<[llvm_i32_ty], []>;
diff --git a/llvm/lib/Target/SPIRV/SPIRVCommandLine.cpp b/llvm/lib/Target/SPIRV/SPIRVCommandLine.cpp
index 734a03ff60141..863c3b6d1f757 100644
--- a/llvm/lib/Target/SPIRV/SPIRVCommandLine.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVCommandLine.cpp
@@ -176,7 +176,8 @@ static const StringMap<SPIRV::Extension::Extension> SPIRVExtensionMap = {
     {"SPV_EXT_image_raw10_raw12",
      SPIRV::Extension::Extension::SPV_EXT_image_raw10_raw12},
     {"SPV_INTEL_unstructured_loop_controls",
-     SPIRV::Extension::Extension::SPV_INTEL_unstructured_loop_controls}};
+     SPIRV::Extension::Extension::SPV_INTEL_unstructured_loop_controls},
+    {"SPV_KHR_abort", SPIRV::Extension::Extension::SPV_KHR_abort}};
 
 bool SPIRVExtensionsParser::parse(cl::Option &O, StringRef ArgName,
                                   StringRef ArgValue, ExtensionSet &Vals) {
diff --git a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
index 7a886de005b88..6e109c1dd74ab 100644
--- a/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
@@ -2253,6 +2253,20 @@ Instruction *SPIRVEmitIntrinsics::visitAtomicCmpXchgInst(AtomicCmpXchgInst &I) {
 Instruction *SPIRVEmitIntrinsics::visitUnreachableInst(UnreachableInst &I) {
   IRBuilder<> B(I.getParent());
   B.SetInsertPoint(&I);
+  // OpAbortKHR is itself a SPIR-V block terminator. If the previous instruction
+  // is a call to llvm.spv.abort, do not emit an additional OpUnreachable, which
+  // would leave the block with two terminators and produce invalid SPIR-V.
+  for (Instruction *Prev = I.getPrevNode(); Prev; Prev = Prev->getPrevNode()) {
+    if (Prev->isDebugOrPseudoInst())
+      continue;
+    auto *CI = dyn_cast<CallInst>(Prev);
+    if (!CI)
+      break;
+    Intrinsic::ID IID = CI->getIntrinsicID();
+    if (IID == Intrinsic::spv_abort)
+      return &I;
+    break;
+  }
   B.CreateIntrinsic(Intrinsic::spv_unreachable, {});
   return &I;
 }
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstrInfo.td b/llvm/lib/Target/SPIRV/SPIRVInstrInfo.td
index 04ff442bbeed2..a1162f2fbc664 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstrInfo.td
+++ b/llvm/lib/Target/SPIRV/SPIRVInstrInfo.td
@@ -641,6 +641,8 @@ let isReturn = 1, hasDelaySlot = 0, isBarrier = 0, isTerminator = 1, isNotDuplic
   def OpReturn: SimpleOp<"OpReturn", 253>;
   def OpReturnValue: Op<254, (outs), (ins ID:$ret), "OpReturnValue $ret">;
   def OpUnreachable: SimpleOp<"OpUnreachable", 255>;
+  def OpAbortKHR: Op<5121, (outs), (ins TYPE:$msg_type, ID:$msg),
+                  "OpAbortKHR $msg_type $msg">;
 }
 def OpLifetimeStart: Op<256, (outs), (ins ID:$ptr, i32imm:$sz), "OpLifetimeStart $ptr $sz">;
 def OpLifetimeStop: Op<257, (outs), (ins ID:$ptr, i32imm:$sz), "OpLifetimeStop $ptr $sz">;
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
index f1e0450bb20f9..7414880ecb05b 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
@@ -350,6 +350,7 @@ class SPIRVInstructionSelector : public InstructionSelector {
 
   bool diagnoseUnsupported(const MachineInstr &I, const Twine &Msg) const;
 
+  bool selectAbort(MachineInstr &I) const;
   bool selectFrameIndex(Register ResVReg, SPIRVTypeInst ResType,
                         MachineInstr &I) const;
   bool selectAllocaArray(Register ResVReg, SPIRVTypeInst ResType,
@@ -4635,6 +4636,8 @@ bool SPIRVInstructionSelector::selectIntrinsic(Register ResVReg,
     BuildMI(BB, I, I.getDebugLoc(), TII.get(SPIRV::OpUnreachable))
         .constrainAllUses(TII, TRI, RBI);
     return true;
+  case Intrinsic::spv_abort:
+    return selectAbort(I);
   case Intrinsic::spv_alloca:
     return selectFrameIndex(ResVReg, ResType, I);
   case Intrinsic::spv_alloca_array:
@@ -6250,6 +6253,64 @@ bool SPIRVInstructionSelector::selectAllocaArray(Register ResVReg,
   return true;
 }
 
+bool SPIRVInstructionSelector::selectAbort(MachineInstr &I) const {
+  if (!STI.canUseExtension(SPIRV::Extension::SPV_KHR_abort))
+    report_fatal_error("OpAbortKHR instruction requires the following "
+                       "SPIR-V extension: SPV_KHR_abort",
+                       false);
+  // The intrinsic is declared as variadic so it can carry composite message
+  // types (vectors and structs) without LLVM IR mangling restrictions, but
+  // OpAbortKHR takes exactly one Message operand.
+  if (I.getNumExplicitOperands() != 2)
+    report_fatal_error("llvm.spv.abort must be called with exactly one "
+                       "message argument",
+                       false);
+  Register MsgReg = I.getOperand(1).getReg();
+  SPIRVTypeInst MsgType = GR.getSPIRVTypeForVReg(MsgReg);
+  assert(MsgType && "Message argument of llvm.spv.abort has no SPIR-V type");
+  // SPV_KHR_abort requires Message Type to be a concrete type. Per the
+  // SPIR-V "Concrete Type" definition, that means a numerical scalar
+  // (int/float), a vector, matrix, or any aggregate (array/struct)
+  // recursively containing only such types. OpTypeBool, OpTypeVoid,
+  // pointers, opaque handles and similar abstract/non-concrete types are
+  // rejected up front rather than emitting invalid SPIR-V. Validate
+  // recursively so that e.g. a struct containing a bool or pointer is also
+  // rejected.
+  SmallVector<SPIRVTypeInst, 4> Worklist{MsgType};
+  while (!Worklist.empty()) {
+    SPIRVTypeInst Ty = Worklist.pop_back_val();
+    switch (Ty->getOpcode()) {
+    case SPIRV::OpTypeInt:
+    case SPIRV::OpTypeFloat:
+      break;
+    case SPIRV::OpTypeVector:
+    case SPIRV::OpTypeMatrix:
+    case SPIRV::OpTypeArray:
+      // Operand 1 holds the element/component type id.
+      Worklist.push_back(
+          GR.getSPIRVTypeForVReg(Ty->getOperand(1).getReg()));
+      break;
+    case SPIRV::OpTypeStruct:
+      // Operands 1..N hold the field type ids.
+      for (unsigned Idx = 1, E = Ty->getNumOperands(); Idx < E; ++Idx)
+        Worklist.push_back(
+            GR.getSPIRVTypeForVReg(Ty->getOperand(Idx).getReg()));
+      break;
+    default:
+      report_fatal_error("llvm.spv.abort message type must be a concrete "
+                         "SPIR-V type (numerical scalar, vector, matrix, "
+                         "or aggregate of such types)",
+                         false);
+    }
+  }
+  MachineBasicBlock &BB = *I.getParent();
+  BuildMI(BB, I, I.getDebugLoc(), TII.get(SPIRV::OpAbortKHR))
+      .addUse(GR.getSPIRVTypeID(MsgType))
+      .addUse(MsgReg)
+      .constrainAllUses(TII, TRI, RBI);
+  return true;
+}
+
 bool SPIRVInstructionSelector::selectFrameIndex(Register ResVReg,
                                                 SPIRVTypeInst ResType,
                                                 MachineInstr &I) const {
diff --git a/llvm/lib/Target/SPIRV/SPIRVModuleAnalysis.cpp b/llvm/lib/Target/SPIRV/SPIRVModuleAnalysis.cpp
index aec9b15df9189..5412e50d6afc5 100644
--- a/llvm/lib/Target/SPIRV/SPIRVModuleAnalysis.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVModuleAnalysis.cpp
@@ -1945,6 +1945,14 @@ void addInstrRequirements(const MachineInstr &MI,
     Reqs.addExtension(SPIRV::Extension::SPV_KHR_shader_clock);
     Reqs.addCapability(SPIRV::Capability::ShaderClockKHR);
     break;
+  case SPIRV::OpAbortKHR:
+    if (!ST.canUseExtension(SPIRV::Extension::SPV_KHR_abort))
+      report_fatal_error("OpAbortKHR instruction requires the "
+                         "following SPIR-V extension: SPV_KHR_abort",
+                         false);
+    Reqs.addExtension(SPIRV::Extension::SPV_KHR_abort);
+    Reqs.addCapability(SPIRV::Capability::AbortKHR);
+    break;
   case SPIRV::OpFunctionPointerCallINTEL:
     if (ST.canUseExtension(SPIRV::Extension::SPV_INTEL_function_pointers)) {
       Reqs.addExtension(SPIRV::Extension::SPV_INTEL_function_pointers);
diff --git a/llvm/lib/Target/SPIRV/SPIRVPrepareFunctions.cpp b/llvm/lib/Target/SPIRV/SPIRVPrepareFunctions.cpp
index a3b44ad6d31d5..7e955e2d6d2df 100644
--- a/llvm/lib/Target/SPIRV/SPIRVPrepareFunctions.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVPrepareFunctions.cpp
@@ -19,6 +19,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "SPIRV.h"
+#include "SPIRVBuiltins.h"
 #include "SPIRVSubtarget.h"
 #include "SPIRVTargetMachine.h"
 #include "SPIRVUtils.h"
@@ -33,6 +34,7 @@
 #include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/IntrinsicsSPIRV.h"
 #include "llvm/Transforms/Utils/Cloning.h"
+#include "llvm/Transforms/Utils/Local.h"
 #include "llvm/Transforms/Utils/LowerMemIntrinsics.h"
 #include <regex>
 
@@ -43,6 +45,7 @@ namespace {
 class SPIRVPrepareFunctions : public ModulePass {
   const SPIRVTargetMachine &TM;
   bool substituteIntrinsicCalls(Function *F);
+  bool substituteAbortKHRCalls(Function *F);
   Function *removeAggregateTypesFromSignature(Function *F);
   bool removeAggregateTypesFromCalls(Function *F);
 
@@ -595,6 +598,62 @@ SPIRVPrepareFunctions::removeAggregateTypesFromSignature(Function *F) {
   return NewF;
 }
 
+// Replace OpenCL/SPIR-V style calls to `__spirv_AbortKHR(message)` with calls
+// to the `llvm.spv.abort` target intrinsic, so that they go through the same
+// instruction-selection path as the intrinsic and get lowered to OpAbortKHR.
+bool SPIRVPrepareFunctions::substituteAbortKHRCalls(Function *F) {
+  if (F->isDeclaration())
+    return false;
+
+  SmallVector<CallInst *> Calls;
+  for (Instruction &I : instructions(F)) {
+    auto *CI = dyn_cast<CallInst>(&I);
+    if (!CI)
+      continue;
+    Function *Callee = CI->getCalledFunction();
+    if (!Callee || Callee->isIntrinsic())
+      continue;
+    StringRef Demangled = Callee->getName();
+    std::string DemangledStr = getOclOrSpirvBuiltinDemangledName(Demangled);
+    if (DemangledStr.empty())
+      continue;
+    std::string BuiltinName = SPIRV::lookupBuiltinNameHelper(DemangledStr);
+    if (StringRef(BuiltinName) != "__spirv_AbortKHR")
+      continue;
+    if (CI->arg_size() != 1)
+      continue;
+    Calls.push_back(CI);
+  }
+
+  if (Calls.empty())
+    return false;
+
+  for (CallInst *CI : Calls) {
+    IRBuilder<> B(CI);
+    Value *Msg = CI->getArgOperand(0);
+    // The OpenCL C ABI may pass aggregate arguments by pointer (byval). In
+    // that case load the underlying value so that OpAbortKHR receives the
+    // composite itself, as required by the SPV_KHR_abort spec ("Message Type
+    // must be a concrete type").
+    if (CI->isByValArgument(0)) {
+      Type *AggTy = CI->getParamByValType(0);
+      Msg = B.CreateLoad(AggTy, Msg);
+    }
+    B.CreateIntrinsic(Intrinsic::spv_abort, {}, {Msg});
+    // OpAbortKHR is itself a SPIR-V function-termination instruction and must
+    // be the last instruction in its block. Drop the original call and
+    // everything that follows it (the OpenCL ABI typically appends stores into
+    // the return slot and a `ret`), and re-terminate the block with
+    // `unreachable`. We use changeToUnreachable so that any successor PHI
+    // nodes have the now-removed predecessor edge cleaned up; otherwise the
+    // IR verifier would reject mismatched PHI incoming entries. The matching
+    // suppression in SPIRVEmitIntrinsics::visitUnreachableInst ensures no
+    // extra OpUnreachable is emitted after OpAbortKHR.
+    changeToUnreachable(CI);
+  }
+  return true;
+}
+
 // Mutates indirect callsites iff if aggregate argument/return types are present
 // with the types replaced by i32 types. The change in types is noted in
 // 'spv.mutated_callsites' metadata for later restoration.
@@ -684,6 +743,7 @@ bool SPIRVPrepareFunctions::runOnModule(Module &M) {
 
   for (Function &F : M) {
     Changed |= substituteIntrinsicCalls(&F);
+    Changed |= substituteAbortKHRCalls(&F);
     Changed |= sortBlocks(F);
     Changed |= removeAggregateTypesFromCalls(&F);
   }
diff --git a/llvm/lib/Target/SPIRV/SPIRVSymbolicOperands.td b/llvm/lib/Target/SPIRV/SPIRVSymbolicOperands.td
index 56608a80f4b23..68ee90f2c6792 100644
--- a/llvm/lib/Target/SPIRV/SPIRVSymbolicOperands.td
+++ b/llvm/lib/Target/SPIRV/SPIRVSymbolicOperands.td
@@ -398,6 +398,10 @@ defm SPV_EXT_image_raw10_raw12 :ExtensionOperand<133, [EnvOpenCL, EnvVulkan]>;
 defm SPV_ALTERA_arbitrary_precision_floating_point: ExtensionOperand<134, [EnvOpenCL]>;
 defm SPV_KHR_fma : ExtensionOperand<135, [EnvVulkan, EnvOpenCL]>;
 defm SPV_INTEL_masked_gather_scatter : ExtensionOperand<136, [EnvOpenCL]>;
+// SPV_KHR_abort is currently only supported in the OpenCL environment.
+// Vulkan/Shader support requires emitting explicit layout decorations on the
+// Message Type, which is not yet implemented.
+defm SPV_KHR_abort : ExtensionOperand<137, [EnvOpenCL]>;
 
 //===----------------------------------------------------------------------===//
 // Multiclass used to define Capabilities enum values and at the same time
@@ -598,6 +602,7 @@ defm GlobalVariableFPGADecorationsINTEL : CapabilityOperand<6189, 0, 0, [SPV_INT
 defm CacheControlsINTEL : CapabilityOperand<6441, 0, 0, [SPV_INTEL_cache_controls], []>;
 defm CooperativeMatrixKHR : CapabilityOperand<6022, 0, 0, [SPV_KHR_cooperative_matrix], []>;
 defm ArithmeticFenceEXT : CapabilityOperand<6144, 0, 0, [SPV_EXT_arithmetic_fence], []>;
+defm AbortKHR : CapabilityOperand<5120, 0, 0, [SPV_KHR_abort], []>;
 defm SplitBarrierINTEL : CapabilityOperand<6141, 0, 0, [SPV_INTEL_split_barrier], []>;
 defm CooperativeMatrixCheckedInstructionsINTEL : CapabilityOperand<6192, 0, 0, [SPV_INTEL_joint_matrix], []>;
 defm CooperativeMatrixPrefetchINTEL : CapabilityOperand<6411, 0, 0, [SPV_INTEL_joint_matrix], []>;
diff --git a/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite-construct.ll b/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite-construct.ll
new file mode 100644
index 0000000000000..b7ab99b47b0cd
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite-construct.ll
@@ -0,0 +1,30 @@
+; RUN: llc -O0 -mtriple=spirv32-unknown-unknown --spirv-ext=+SPV_KHR_abort %s -o - | FileCheck %s
+; RUN: llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %s -o - -filetype=obj | spirv-val %}
+
+;; Verify that OpAbortKHR can consume a composite produced by
+;; OpCompositeConstruct, mirroring the SPV_KHR_abort spec example which builds
+;; the message via OpCompositeConstruct.
+
+; CHECK-DAG: OpCapability AbortKHR
+; CHECK-DAG: OpExtension "SPV_KHR_abort"
+; CHECK-DAG: %[[#I32:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#V2:]] = OpTypeVector %[[#I32]] 2
+
+; CHECK:     %[[#CC:]] = OpCompositeConstruct %[[#V2]] %{{[0-9]+}} %{{[0-9]+}}
+; CHECK:     OpAbortKHR %[[#V2]] %[[#CC]]
+; CHECK-NOT: OpUnreachable
+
+declare void @llvm.spv.abort(...) #0
+
+define spir_kernel void @abort_composite_construct(i32 %a, i32 %b) {
+entry:
+  %va = insertelement <1 x i32> poison, i32 %a, i32 0
+  %vb = insertelement <1 x i32> poison, i32 %b, i32 0
+  %v = shufflevector <1 x i32> %va, <1 x i32> %vb,
+                     <2 x i32> <i32 0, i32 1>
+  call void (...) @llvm.spv.abort(<2 x i32> %v)
+  unreachable
+}
+
+attributes #0 = { noreturn }
diff --git a/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite.ll b/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite.ll
new file mode 100644
index 0000000000000..f8c779dc7b891
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-composite.ll
@@ -0,0 +1,51 @@
+; RUN: llc -O0 -mtriple=spirv32-unknown-unknown --spirv-ext=+SPV_KHR_abort %s -o - | FileCheck %s
+; RUN: llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %s -o - -filetype=obj | spirv-val %}
+
+; Verify that OpAbortKHR can consume a composite (struct or vector) message,
+; built up via OpCompositeInsert, mirroring the example in the SPV_KHR_abort
+; specification.
+
+; CHECK-DAG: OpCapability AbortKHR
+; CHECK-DAG: OpExtension "SPV_KHR_abort"
+
+; CHECK-DAG: %[[#I32:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#F32:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#STRUCT:]] = OpTypeStruct %[[#I32]] %[[#I32]] %[[#F32]]
+; CHECK-DAG: %[[#VEC:]] = OpTypeVector %[[#I32]] 4
+
+; CHECK: %[[#S0:]] = OpCompositeInsert %[[#STRUCT]] %{{[0-9]+}} %{{[0-9]+}} 0
+; CHECK: %[[#S1:]] = OpCompositeInsert %[[#STRUCT]] %{{[0-9]+}} %[[#S0]] 1
+; CHECK: %[[#S2:]] = OpCompositeInsert %[[#STRUCT]] %{{[0-9]+}} %[[#S1]] 2
+; CHECK: OpAbortKHR %[[#STRUCT]] %[[#S2]]
+
+; CHECK: %[[#V0:]] = OpCompositeInsert %[[#VEC]] %{{[0-9]+}} %{{[0-9]+}} 0
+; CHECK: %[[#V1:]] = OpCompositeInsert %[[#VEC]] %{{[0-9]+}} %[[#V0]] 1
+; CHECK: %[[#V2:]] = OpCompositeInsert %[[#VEC]] %{{[0-9]+}} %[[#V1]] 2
+; CHECK: %[[#V3:]] = OpCompositeInsert %[[#VEC]] %{{[0-9]+}} %[[#V2]] 3
+; CHECK: OpAbortKHR %[[#VEC]] %[[#V3]]
+
+%struct.Msg = type { i32, i32, float }
+
+declare void @llvm.spv.abort(...) #0
+
+define spir_kernel void @abort_with_struct(i32 %x, i32 %y, float %z) {
+entry:
+  %m0 = insertvalue %struct.Msg poison, i32 %x, 0
+  %m1 = insertvalue %struct.Msg %m0, i32 %y, 1
+  %m2 = insertvalue %struct.Msg %m1, float %z, 2
+  call void (...) @llvm.spv.abort(%struct.Msg %m2)
+  unreachable
+}
+
+define spir_kernel void @abort_with_vector(i32 %a, i32 %b, i32 %c, i32 %d) {
+entry:
+  %v0 = insertelement <4 x i32> poison, i32 %a, i32 0
+  %v1 = insertelement <4 x i32> %v0, i32 %b, i32 1
+  %v2 = insertelement <4 x i32> %v1, i32 %c, i32 2
+  %v3 = insertelement <4 x i32> %v2, i32 %d, i32 3
+  call void (...) @llvm.spv.abort(<4 x i32> %v3)
+  unreachable
+}
+
+attributes #0 = { noreturn }
diff --git a/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-invalid-message-type.ll b/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-invalid-message-type.ll
new file mode 100644
index 0000000000000..d491ac70d80de
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/abort-invalid-message-type.ll
@@ -0,0 +1,61 @@
+; RUN: split-file %s %t
+; RUN: not llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %t/top-level.ll -o /dev/null 2>&1 | FileCheck %s
+; RUN: not llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %t/nested-struct.ll -o /dev/null 2>&1 | FileCheck %s
+; RUN: not llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %t/nested-array.ll -o /dev/null 2>&1 | FileCheck %s
+; RUN: not llc -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_abort %t/top-level-bool.ll -o /dev/null 2>&1 | FileCheck %s
+; RUN: not llc -...
[truncated]

@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown

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

@@ -6250,6 +6253,63 @@ bool SPIRVInstructionSelector::selectAllocaArray(Register ResVReg,
return true;
}

bool SPIRVInstructionSelector::selectAbort(MachineInstr &I) const {
if (!STI.canUseExtension(SPIRV::Extension::SPV_KHR_abort))

@michalpaszkowski michalpaszkowski Apr 21, 2026

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.

Should we also check for/enable implicitly SPV_KHR_constant_data or the ConstantDataKHR (not necessarily here)? I think ConstantDataKHR has not been added to the SPIR-V backend

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.

The extension formally requires the SPV_KHR_constand_data, but there are no usage requirements or validation rules that would make the constant data extension mandatory. Let's keep the implementation as-is for now, and clarify the requirements with the SPV_KHR_abort extension authors.

Comment on lines +6280 to +6298
while (!Worklist.empty()) {
SPIRVTypeInst Ty = Worklist.pop_back_val();
switch (Ty->getOpcode()) {
case SPIRV::OpTypeInt:
case SPIRV::OpTypeFloat:
break;
case SPIRV::OpTypeVector:
case SPIRV::OpTypeMatrix:
case SPIRV::OpTypeArray:
// Operand 1 holds the element/component type id.
Worklist.push_back(GR.getSPIRVTypeForVReg(Ty->getOperand(1).getReg()));
break;
case SPIRV::OpTypeStruct:
// Operands 1..N hold the field type ids.
for (unsigned Idx = 1, E = Ty->getNumOperands(); Idx < E; ++Idx)
Worklist.push_back(
GR.getSPIRVTypeForVReg(Ty->getOperand(Idx).getReg()));
break;
default:

@michalpaszkowski michalpaszkowski Apr 21, 2026

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.

Could this also be a physical pointer (since it can be any concrete type)?

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.

Implemented: d0da520

Intrinsic::ID IID = CI->getIntrinsicID();
if (IID == Intrinsic::spv_abort)
return &I;
break;

@michalpaszkowski michalpaszkowski Apr 21, 2026

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.

I think this will emit spv_unreachable if there is any real instruction between spv_abort and unreachable. Not sure if such situation can happen in reality, but we could add an assert.
E.g.:

call @llvm.spv.abort(...)
%tmp = freeze i32 poison
unreachable

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.

Implemented: d0da520

Comment on lines +2270 to +2278
#ifndef NDEBUG
for (Instruction *P = I.getPrevNode(); P; P = P->getPrevNode()) {
auto *CI = dyn_cast<CallInst>(P);
if (CI && CI->getIntrinsicID() == Intrinsic::spv_abort)
llvm_unreachable("llvm.spv.abort must be the last non-debug instruction "
"before its block's `unreachable`; OpAbortKHR is itself "
"a SPIR-V block terminator");
}
#endif

@jmmartinez jmmartinez Apr 23, 2026

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 kind of check is for the verifier and should not be in here. You should be able to assume that the IR is well formed.

Or if it's only meant for debug builds, like an assertion, then use an actual assertion.

PS: I'll do the later.


On a totally different aspect, instead of iteration backwards, you can iterate from the beginning of the block until I and make the code easier to read.

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.

Also, I'm not sure the check is correct. Look at the example I posted in the translator PR: KhronosGroup/SPIRV-LLVM-Translator#3691 (comment)

Why would that be invalid? I think we should be able to gracefully handle that instead of asserting.

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've implemented the handling on the stage, when the __spirv_AbortKHR built-in function is translated to the llvm.spv.abort intrinsic. Also, I've re-written the check to an assertion: 6bacaf1

Comment on lines +6258 to +6261
report_fatal_error("OpAbortKHR instruction requires the following "
"SPIR-V extension: SPV_KHR_abort",
false);
// The intrinsic is declared as variadic so it can carry composite message

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 shouldn't hit a fatal error. This should call SPIRVInstructionSelector::diagnoseUnsupported and return false to trigger a failure to select.

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.

Done: 6bacaf1

Comment on lines +6264 to +6267
if (I.getNumExplicitOperands() != 2)
report_fatal_error("llvm.spv.abort must be called with exactly one "
"message argument",
false);

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 kind of check should never happen in well-formed LLVM-IR right?

You could do it in the verifier. Or put an assert(I.getNumExplicitOperands() == 2); for the developers.

@@ -6250,6 +6253,63 @@ bool SPIRVInstructionSelector::selectAllocaArray(Register ResVReg,
return true;
}

bool SPIRVInstructionSelector::selectAbort(MachineInstr &I) const {
if (!STI.canUseExtension(SPIRV::Extension::SPV_KHR_abort))

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 don't think you should be doing this check in here. It is done already in llvm/lib/Target/SPIRV/SPIRVModuleAnalysis.cpp. But I get that we're inconsistent with this on this file.

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.

There are similar checks already implemented for the selectStackSave and selectStackRestore functions, so I'd keep the check here. I've rewritten it from the fatal error to the diagnoseUnsupported.

Comment on lines +609 to +626
for (Instruction &I : instructions(F)) {
auto *CI = dyn_cast<CallInst>(&I);
if (!CI)
continue;
Function *Callee = CI->getCalledFunction();
if (!Callee || Callee->isIntrinsic())
continue;
StringRef Demangled = Callee->getName();
std::string DemangledStr = getOclOrSpirvBuiltinDemangledName(Demangled);
if (DemangledStr.empty())
continue;
std::string BuiltinName = SPIRV::lookupBuiltinNameHelper(DemangledStr);
if (StringRef(BuiltinName) != "__spirv_AbortKHR")
continue;
if (CI->arg_size() != 1)
continue;
Calls.push_back(CI);
}

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.

You should do this the other way around, scan the functions in the module looking for __spirv_AbortKHR; then do the replacement on its Users.

Otherwise we scan the whole module even if __spirv_AbortKHR is not used.

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.

Done: 6bacaf1

Comment thread llvm/include/llvm/IR/IntrinsicsSPIRV.td Outdated
@@ -39,6 +39,7 @@ let TargetPrefix = "spv" in {
def int_spv_selection_merge : Intrinsic<[], [llvm_any_ty, llvm_i32_ty], [ImmArg<ArgIndex<1>>]>;
def int_spv_cmpxchg : Intrinsic<[llvm_i32_ty], [llvm_any_ty, llvm_vararg_ty]>;
def int_spv_unreachable : Intrinsic<[], []>;
def int_spv_abort : Intrinsic<[], [llvm_vararg_ty], [IntrNoReturn]>;

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 if we parametrize the intrinsic by type, instead of taking a vararg?

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 do believe that we need to implement constant data extension before abort so we could use constant data in the arg.

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.

On a second thought, not sure I agree. If instead of a constant string we would like to pass simply a constant int as message, I think that would be valid for OpAbortKHR, wouldn't it? So the constant data extension wouldn't be required for that, would it? Also, I'm concerned about the fact that this accepts any number of arguments, while OpAbortKHR only accepts one: the message --the type of that one should be implicit. Should we use llvm_any_ty instead?

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 withdraw my comment requiring a constant data. Lets have some implementation experience to may be provide feedback to the specification.

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've converted the intrinsic operand type from the vararg to the llvm_any_ty: 6bacaf1

return false;

SmallVector<CallInst *> Calls;
for (Instruction &I : instructions(F)) {

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 think calls to @llvm.trap should also translate to a call to spv_abort (if and only if the extension is available). Currently a call to @llvm.trap leads to a no-op that is against the langref specification.

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.

+1 to this. We can leave it as todo for now, but ultimately we should be lowering llvm.trap to spirv.abort. The Q would be: "what kind of message we should pass in this case?", may be something like: "abort is caused by llvm.trap with no debug string" is good enough.

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 would like the translation to be implemented in this very patch, if possible. It's tightly related, so we may as well have it all together. Regarding the message, we could try and be a little more verbose and pass maybe something more meaningful. I'd say maybe a combination of the instruction, basic block, function and module, so that at least it's possible to know where the abort happened. I'd be happy to have that in a follow up, though. Something basic is good to start with.

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 emit something like OpAbortKHR Int32 0 in this case, and let the device compiler to generate its own message. @jmmartinez, @MrSidims, @maarquitos14, what do you think?

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.

That works for me as a starting point. We can follow up in the future.

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.

Sounds good to me. If anything, we can change it to something else later.

PS: I'll be on vacations so I won't be able to go further with the review. But don't wait for my approval to land this change.

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.

Implemented both llvm.trap and llvm.ubsantrap as follows:

  • The llvm.ubsantrap is lowered into OpAbortKHR with the ubsantrap operand zero-extended to 32 bits and used as the abort "message".
  • For the llvm.trap message, the 32-bit constant 0xFFFFFFFF is used.

See the details: 6bacaf1

Comment thread llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp Outdated
// rejected up front rather than emitting invalid SPIR-V. Validate
// recursively so that e.g. a struct containing a bool is also rejected.
SmallVector<SPIRVTypeInst, 4> Worklist{MsgType};
while (!Worklist.empty()) {

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 think this deserves its own function.

@jmmartinez

Copy link
Copy Markdown
Contributor

This PR may be related to #178983 and could be a way to implement the support for @llvm.trap in SPIRV.

@s-perron s-perron 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.

I took a quick look. LGTM. Enough people are looking at the details, so I won't.

Comment thread llvm/include/llvm/IR/IntrinsicsSPIRV.td Outdated
@@ -39,6 +39,7 @@ let TargetPrefix = "spv" in {
def int_spv_selection_merge : Intrinsic<[], [llvm_any_ty, llvm_i32_ty], [ImmArg<ArgIndex<1>>]>;
def int_spv_cmpxchg : Intrinsic<[llvm_i32_ty], [llvm_any_ty, llvm_vararg_ty]>;
def int_spv_unreachable : Intrinsic<[], []>;
def int_spv_abort : Intrinsic<[], [llvm_vararg_ty], [IntrNoReturn]>;

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.

On a second thought, not sure I agree. If instead of a constant string we would like to pass simply a constant int as message, I think that would be valid for OpAbortKHR, wouldn't it? So the constant data extension wouldn't be required for that, would it? Also, I'm concerned about the fact that this accepts any number of arguments, while OpAbortKHR only accepts one: the message --the type of that one should be implicit. Should we use llvm_any_ty instead?

Comment on lines +2270 to +2278
#ifndef NDEBUG
for (Instruction *P = I.getPrevNode(); P; P = P->getPrevNode()) {
auto *CI = dyn_cast<CallInst>(P);
if (CI && CI->getIntrinsicID() == Intrinsic::spv_abort)
llvm_unreachable("llvm.spv.abort must be the last non-debug instruction "
"before its block's `unreachable`; OpAbortKHR is itself "
"a SPIR-V block terminator");
}
#endif

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.

Also, I'm not sure the check is correct. Look at the example I posted in the translator PR: KhronosGroup/SPIRV-LLVM-Translator#3691 (comment)

Why would that be invalid? I think we should be able to gracefully handle that instead of asserting.

report_fatal_error("OpAbortKHR instruction requires the following "
"SPIR-V extension: SPV_KHR_abort",
false);
// The intrinsic is declared as variadic so it can carry composite message

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.

Maybe the problem is that it was declared as variadic in the first place. Why do we need that?

return false;

SmallVector<CallInst *> Calls;
for (Instruction &I : instructions(F)) {

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 would like the translation to be implemented in this very patch, if possible. It's tightly related, so we may as well have it all together. Regarding the message, we could try and be a little more verbose and pass maybe something more meaningful. I'd say maybe a combination of the instruction, basic block, function and module, so that at least it's possible to know where the abort happened. I'd be happy to have that in a follow up, though. Something basic is good to start with.

vmustya added 3 commits April 29, 2026 15:54
# Conflicts:
#	llvm/lib/Target/SPIRV/SPIRVCommandLine.cpp
#	llvm/lib/Target/SPIRV/SPIRVPrepareFunctions.cpp
#	llvm/lib/Target/SPIRV/SPIRVSymbolicOperands.td
@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 194852 tests passed
  • 5168 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 134337 tests passed
  • 3229 tests skipped

✅ The build succeeded and all tests passed.

@maarquitos14 maarquitos14 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, just a few nits.

Comment thread llvm/lib/Target/SPIRV/SPIRVEmitIntrinsics.cpp
Comment thread llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp Outdated
Comment thread llvm/test/CodeGen/SPIRV/extensions/SPV_KHR_abort/ubsantrap-basic.ll
@michalpaszkowski michalpaszkowski merged commit 75428e9 into llvm:main May 6, 2026
12 checks passed
moar55 pushed a commit to moar55/llvm-project that referenced this pull request May 12, 2026
This commit adds support for the SPV_KHR_abort extension in the SPIRV
backend. The extension allows shaders to abort execution with a custom
message.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Marcos Maronas <mmaronas@amd.com>
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request May 19, 2026
This commit adds support for the SPV_KHR_abort extension in the SPIRV
backend. The extension allows shaders to abort execution with a custom
message.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Marcos Maronas <mmaronas@amd.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.

7 participants