[spirv] Add support for linking SPIR-V modules (#6557)

Actually, Vulkan flavored SPIR-V does not have linking in the
conventional sense. For example, there is no cross-module symbol
reference and symbol resolution and such. It's more just combining
all SPIR-V modules into the one, with multiple entry points.

This commit enables combining multiple SPIR-V modules into one and
serialize it. Right now we only combine SPIR-V modules using the
same interface (i.e., binding layout). This avoids handling
different descriptor set layouts and it also is forward looking,
as we would like to have two level of linking/combining eventually.

Fixes https://github.com/google/iree/issues/3604
diff --git a/iree/compiler/Dialect/HAL/IR/HALOps.cpp b/iree/compiler/Dialect/HAL/IR/HALOps.cpp
index e172be6..937233e 100644
--- a/iree/compiler/Dialect/HAL/IR/HALOps.cpp
+++ b/iree/compiler/Dialect/HAL/IR/HALOps.cpp
@@ -9,6 +9,7 @@
 #include "iree/compiler/Dialect/HAL/IR/HALTypes.h"
 #include "iree/compiler/Dialect/IREE/IR/IREETypes.h"
 #include "iree/compiler/Dialect/Shape/IR/Builders.h"
+#include "llvm/ADT/Hashing.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/SMLoc.h"
 #include "mlir/Dialect/StandardOps/IR/Ops.h"
@@ -1485,6 +1486,15 @@
          });
 }
 
+llvm::hash_code InterfaceOp::getInterfaceHash() {
+  auto range = llvm::map_range(getBlock().getOps<InterfaceBindingOp>(),
+                               [](InterfaceBindingOp bindingOp) {
+                                 return bindingOp.getDescriptorHash();
+                               });
+  return llvm::hash_combine(
+      push_constants(), llvm::hash_combine_range(range.begin(), range.end()));
+}
+
 //===----------------------------------------------------------------------===//
 // hal.interface.binding
 //===----------------------------------------------------------------------===//
@@ -1537,6 +1547,13 @@
                                      });
 }
 
+llvm::hash_code InterfaceBindingOp::getDescriptorHash() {
+  // Use the unwrapped attribute accessors so that we can have determinstic
+  // hashes. Hashing against the wrapped attributes are hashing against pointer
+  // values, which change per run.
+  return llvm::hash_combine(set(), binding(), type(), access());
+}
+
 //===----------------------------------------------------------------------===//
 // hal.interface.binding.subspan
 //===----------------------------------------------------------------------===//
diff --git a/iree/compiler/Dialect/HAL/IR/HALOps.td b/iree/compiler/Dialect/HAL/IR/HALOps.td
index ac62c81..282253d 100644
--- a/iree/compiler/Dialect/HAL/IR/HALOps.td
+++ b/iree/compiler/Dialect/HAL/IR/HALOps.td
@@ -2272,6 +2272,10 @@
     // Returns true if the all bindings in the interface match exactly those
     // in |other| (including order).
     bool isEquivalentTo(IREE::HAL::InterfaceOp other);
+
+    // Returns a hash for the interface, considering the push constant and
+    // all bindings.
+    llvm::hash_code getInterfaceHash();
   }];
 }
 
@@ -2311,6 +2315,12 @@
     HAL_DescriptorTypeAttr:$type,
     HAL_MemoryAccessBitfieldAttr:$access
   );
+
+  let extraClassDeclaration = [{
+    /// Returns a hash for the descriptor, considering the set, binding,
+    /// type, and access.
+    llvm::hash_code getDescriptorHash();
+  }];
 }
 
 def HAL_InterfaceWorkgroupIDOp : HAL_PureOp<"interface.workgroup.id", [
diff --git a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/BUILD b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/BUILD
index f2dbc29..d896b64 100644
--- a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/BUILD
+++ b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/BUILD
@@ -47,6 +47,7 @@
         "@llvm-project//mlir:LinalgOps",
         "@llvm-project//mlir:Parser",
         "@llvm-project//mlir:SPIRVDialect",
+        "@llvm-project//mlir:SPIRVModuleCombiner",
         "@llvm-project//mlir:SPIRVSerialization",
         "@llvm-project//mlir:Support",
         "@llvm-project//mlir:VectorOps",
diff --git a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/CMakeLists.txt b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/CMakeLists.txt
index e74b7ee..13c0474 100644
--- a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/CMakeLists.txt
+++ b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/CMakeLists.txt
@@ -29,6 +29,7 @@
     MLIRLinalg
     MLIRParser
     MLIRSPIRV
+    MLIRSPIRVModuleCombiner
     MLIRSPIRVSerialization
     MLIRSupport
     MLIRVector
diff --git a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/VulkanSPIRVTarget.cpp b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/VulkanSPIRVTarget.cpp
index c46d4c2..23282c1 100644
--- a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/VulkanSPIRVTarget.cpp
+++ b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/VulkanSPIRVTarget.cpp
@@ -14,17 +14,23 @@
 #include "iree/compiler/Dialect/Vulkan/Utils/TargetEnvironment.h"
 #include "iree/compiler/Utils/FlatbufferUtils.h"
 #include "iree/schemas/spirv_executable_def_builder.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/CommandLine.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/Path.h"
 #include "mlir/Dialect/Affine/IR/AffineOps.h"
 #include "mlir/Dialect/GPU/GPUDialect.h"
 #include "mlir/Dialect/Linalg/IR/LinalgTypes.h"
 #include "mlir/Dialect/SPIRV/IR/SPIRVDialect.h"
 #include "mlir/Dialect/SPIRV/IR/SPIRVOps.h"
 #include "mlir/Dialect/SPIRV/IR/TargetAndABI.h"
+#include "mlir/Dialect/SPIRV/Linking/ModuleCombiner.h"
 #include "mlir/Dialect/Vector/VectorOps.h"
 #include "mlir/IR/BlockAndValueMapping.h"
 #include "mlir/IR/Builders.h"
+#include "mlir/IR/BuiltinOps.h"
+#include "mlir/IR/SymbolTable.h"
 #include "mlir/Parser.h"
 #include "mlir/Target/SPIRV/Serialization.h"
 
@@ -33,6 +39,32 @@
 namespace IREE {
 namespace HAL {
 
+namespace {
+llvm::Optional<FileLineColLoc> findFirstFileLoc(Location baseLoc) {
+  if (auto loc = baseLoc.dyn_cast<FusedLoc>()) {
+    for (auto &childLoc : loc.getLocations()) {
+      auto childResult = findFirstFileLoc(childLoc);
+      if (childResult) return childResult;
+    }
+  } else if (auto loc = baseLoc.dyn_cast<FileLineColLoc>()) {
+    return loc;
+  }
+  return llvm::None;
+}
+
+std::string guessModuleName(mlir::ModuleOp moduleOp) {
+  std::string moduleName =
+      moduleOp.getName().hasValue() ? moduleOp.getName().getValue().str() : "";
+  if (!moduleName.empty()) return moduleName;
+  auto loc = findFirstFileLoc(moduleOp.getLoc());
+  if (loc.hasValue()) {
+    return llvm::sys::path::stem(loc.getValue().getFilename()).str();
+  } else {
+    return "spirv_module";
+  }
+}
+}  // namespace
+
 VulkanSPIRVTargetOptions getVulkanSPIRVTargetOptionsFromFlags() {
   // TODO(antiagainst): Enable option categories once the following bug is
   // fixed: https://bugs.llvm.org/show_bug.cgi?id=44223 static
@@ -123,10 +155,127 @@
     buildSPIRVCodegenPassPipeline(passManager, options_.codegenOptions);
   }
 
+  LogicalResult linkExecutables(mlir::ModuleOp moduleOp) override {
+    // Note: Vulkan flavored SPIR-V does not have linking in the conventional
+    // sense. For example, there is no cross-module symbol reference and symbol
+    // resolution and such. It's more just combining all SPIR-V modules into the
+    // one, with multiple entry points.
+
+    // 1. Create source executable groups according to their executable
+    // interface. We only combine executables in the same group.
+
+    // Map from an executable interface's hash to all source executables having
+    // that interface.
+    llvm::DenseMap<llvm::hash_code, SmallVector<IREE::HAL::ExecutableOp, 4>>
+        sourceExecutableOpGroups;
+
+    int numExecutables = 0;
+    for (auto op : moduleOp.getOps<IREE::HAL::ExecutableOp>()) {
+      auto interfaceOps =
+          llvm::to_vector<1>(op.getBlock().getOps<IREE::HAL::InterfaceOp>());
+      if (!llvm::hasSingleElement(interfaceOps)) {
+        return op->emitError("only one hal.interface is supported now");
+      }
+
+      llvm::hash_code hash = interfaceOps.front().getInterfaceHash();
+      sourceExecutableOpGroups[hash].push_back(op);
+
+      ++numExecutables;
+    }
+    if (numExecutables <= 1) return success();
+
+    SymbolTable symbolTable(moduleOp);
+
+    auto sharedTargetsAttr = getExecutableTargets(moduleOp.getContext());
+    if (llvm::size(sharedTargetsAttr) != 1) {
+      return moduleOp.emitError("only one executable target is supported now");
+    }
+
+    auto sharedTargetAttr = sharedTargetsAttr.getValue()
+                                .front()
+                                .cast<IREE::HAL::ExecutableTargetAttr>();
+
+    // Guess a module name, if needed, to make the output files readable.
+    auto moduleName = guessModuleName(moduleOp);
+
+    // 2. Create "linked" executables for each source executable group.
+    // This just pulls in spv.module ops that should be combined into the same
+    // hal.executable.variant inner module.
+
+    SmallVector<mlir::ModuleOp, 8> innerModuleOps;
+    innerModuleOps.reserve(sourceExecutableOpGroups.size());
+    for (auto hashExecutablePair : sourceExecutableOpGroups) {
+      llvm::hash_code hash = hashExecutablePair.first;
+      const auto &sourceExecutableOps = hashExecutablePair.second;
+
+      // Just one executable for this group. No need to link.
+      if (sourceExecutableOps.size() == 1) continue;
+
+      OpBuilder builder(moduleOp.getContext());
+
+      // Create a new "linked" hal.executable for collecting all source
+      // executables in this group.
+      std::string linkedExecutableName =
+          llvm::formatv("{0}_linked_{1}", moduleName, name());
+      auto linkedExecutableOp = builder.create<IREE::HAL::ExecutableOp>(
+          moduleOp.getLoc(), linkedExecutableName);
+      symbolTable.insert(linkedExecutableOp, moduleOp.getBody()->begin());
+
+      // Add our hal.executable.variant with an empty module.
+      builder.setInsertionPointToStart(linkedExecutableOp.getBody());
+      auto linkedTargetOp = builder.create<IREE::HAL::ExecutableVariantOp>(
+          moduleOp.getLoc(), sharedTargetAttr.getSymbolNameFragment(),
+          sharedTargetAttr);
+      builder.setInsertionPoint(&linkedTargetOp.getBlock().back());
+      innerModuleOps.push_back(
+          builder.create<mlir::ModuleOp>(moduleOp.getLoc()));
+
+      // Try linking together all executables in moduleOp.
+      if (failed(linkExecutablesInto(
+              moduleOp, sourceExecutableOps, linkedExecutableOp, linkedTargetOp,
+              [](mlir::ModuleOp moduleOp) { return moduleOp; }, builder)))
+        return failure();
+    }
+
+    // 3. Now we can have multiple spv.module ops in the same
+    // hal.executable.variant inner module. Combining them into one.
+
+    auto symbolRenameListener = [](spirv::ModuleOp symbolTable,
+                                   StringRef oldSymbol, StringRef newSymbol) {
+      // We don't care about global variable renaming. There should not exist
+      // duplicated functions. But double check that.
+      if (Operation *op = SymbolTable::lookupSymbolIn(symbolTable, oldSymbol)) {
+        assert(!isa<spirv::FuncOp>(op) &&
+               "found duplicated spv.func names when linking!");
+      }
+    };
+
+    for (mlir::ModuleOp innerModule : innerModuleOps) {
+      auto spvModules =
+          llvm::to_vector<4>(innerModule.getBody()->getOps<spirv::ModuleOp>());
+      if (spvModules.size() <= 1) continue;
+
+      OpBuilder builder(innerModule);
+      auto newModule = builder.create<mlir::ModuleOp>(innerModule.getLoc());
+
+      // Create the combined spv.module op and erase the old inner module.
+      builder.setInsertionPointToStart(newModule.getBody());
+      spirv::combine(spvModules, builder, symbolRenameListener).release();
+      innerModule.erase();
+    }
+
+    return success();
+  }
+
   LogicalResult serializeExecutable(IREE::HAL::ExecutableVariantOp variantOp,
                                     OpBuilder &executableBuilder) override {
     ModuleOp innerModuleOp = variantOp.getInnerModule();
-    auto spvModuleOp = *innerModuleOp.getOps<spirv::ModuleOp>().begin();
+    auto spirvModuleOps = innerModuleOp.getOps<spirv::ModuleOp>();
+    if (!llvm::hasSingleElement(spirvModuleOps)) {
+      return variantOp.emitError()
+             << "should only contain exactly one spv.module op";
+    }
+    auto spvModuleOp = *spirvModuleOps.begin();
 
     FlatbufferBuilder builder;
     iree_SpirVExecutableDef_start_as_root(builder);
diff --git a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/BUILD b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/BUILD
index 5a93026..524af16 100644
--- a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/BUILD
+++ b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/BUILD
@@ -17,6 +17,7 @@
     name = "lit",
     srcs = enforce_glob(
         [
+            "linking.mlir",
             "smoketest.mlir",
         ],
         include = ["*.mlir"],
diff --git a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/CMakeLists.txt b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/CMakeLists.txt
index c2c0cf1..7c55f8b 100644
--- a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/CMakeLists.txt
+++ b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/CMakeLists.txt
@@ -14,6 +14,7 @@
   NAME
     lit
   SRCS
+    "linking.mlir"
     "smoketest.mlir"
   DATA
     iree::tools::IreeFileCheck
diff --git a/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/linking.mlir b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/linking.mlir
new file mode 100644
index 0000000..379836e
--- /dev/null
+++ b/iree/compiler/Dialect/HAL/Target/VulkanSPIRV/test/linking.mlir
@@ -0,0 +1,185 @@
+// RUN: iree-opt -split-input-file -iree-hal-link-target-executables='target=vulkan-spirv'  %s | IreeFileCheck %s
+
+#executable_target_vulkan_spirv_fb = #hal.executable.target<"vulkan", "vulkan-spirv-fb">
+
+hal.executable @call_dispatch_0 attributes {sym_visibility = "private"} {
+  hal.interface @io {
+    hal.interface.binding @s0b0_ro_external, set=0, binding=0, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b1_rw_external, set=0, binding=1, type="StorageBuffer", access="Read|Write"
+  }
+  hal.executable.variant @vulkan_spirv_fb, target = #executable_target_vulkan_spirv_fb {
+    hal.executable.entry_point @call_dispatch_0 attributes {interface = @io, ordinal = 0 : index}
+    module {
+      spv.module Logical GLSL450 requires #spv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]> {
+        spv.func @call_dispatch_0() "None" {
+          spv.Return
+        }
+        spv.EntryPoint "GLCompute" @call_dispatch_0
+        spv.ExecutionMode @call_dispatch_0 "LocalSize", 32, 1, 1
+      }
+      hal.interface @io attributes {sym_visibility = "private"} {
+        hal.interface.binding @s0b0_ro_external, set=0, binding=0, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b1_rw_external, set=0, binding=1, type="StorageBuffer", access="Read|Write"
+      }
+    }
+  }
+}
+hal.executable @call_dispatch_1 attributes {sym_visibility = "private"} {
+  hal.interface @io {
+    hal.interface.binding @s0b0_ro_constant, set=0, binding=0, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b1_ro_external, set=0, binding=1, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b2_xw_external, set=0, binding=2, type="StorageBuffer", access="Write|Discard"
+  }
+  hal.executable.variant @vulkan_spirv_fb, target = #executable_target_vulkan_spirv_fb {
+    hal.executable.entry_point @call_dispatch_1 attributes {interface = @io, ordinal = 0 : index}
+    module {
+      spv.module Logical GLSL450 requires #spv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]> {
+        spv.func @call_dispatch_1() "None" {
+          spv.Return
+        }
+        spv.EntryPoint "GLCompute" @call_dispatch_1
+        spv.ExecutionMode @call_dispatch_1 "LocalSize", 4, 4, 1
+      }
+      hal.interface @io attributes {sym_visibility = "private"} {
+        hal.interface.binding @s0b0_ro_constant, set=0, binding=0, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b1_ro_external, set=0, binding=1, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b2_xw_external, set=0, binding=2, type="StorageBuffer", access="Write|Discard"
+      }
+    }
+  }
+}
+hal.executable @call_dispatch_2 attributes {sym_visibility = "private"} {
+  hal.interface @io {
+    hal.interface.binding @s0b0_ro_external, set=0, binding=0, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b1_rw_external, set=0, binding=1, type="StorageBuffer", access="Read|Write"
+  }
+  hal.executable.variant @vulkan_spirv_fb, target = #executable_target_vulkan_spirv_fb {
+    hal.executable.entry_point @call_dispatch_2 attributes {interface = @io, ordinal = 0 : index}
+    module {
+      spv.module Logical GLSL450 requires #spv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]> {
+        spv.func @call_dispatch_2() "None" {
+          spv.Return
+        }
+        spv.EntryPoint "GLCompute" @call_dispatch_2
+        spv.ExecutionMode @call_dispatch_2 "LocalSize", 32, 1, 1
+      }
+      hal.interface @io attributes {sym_visibility = "private"} {
+        hal.interface.binding @s0b0_ro_external, set=0, binding=0, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b1_rw_external, set=0, binding=1, type="StorageBuffer", access="Read|Write"
+      }
+    }
+  }
+}
+hal.executable @call_dispatch_3 attributes {sym_visibility = "private"} {
+  hal.interface @io {
+    hal.interface.binding @s0b0_ro_constant, set=0, binding=0, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b1_ro_external, set=0, binding=1, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b2_xw_external, set=0, binding=2, type="StorageBuffer", access="Write|Discard"
+  }
+  hal.executable.variant @vulkan_spirv_fb, target = #executable_target_vulkan_spirv_fb {
+    hal.executable.entry_point @call_dispatch_3 attributes {interface = @io, ordinal = 0 : index} {
+    ^bb0(%arg0: index, %arg1: index, %arg2: index):  // no predecessors
+      %c1 = constant 1 : index
+      %c56 = constant 56 : index
+      %c56_0 = constant 56 : index
+      hal.return %c1, %c56, %c56_0 : index, index, index
+    }
+    module {
+      spv.module Logical GLSL450 requires #spv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]> {
+        spv.func @call_dispatch_3() "None" {
+          spv.Return
+        }
+        spv.EntryPoint "GLCompute" @call_dispatch_3
+        spv.ExecutionMode @call_dispatch_3 "LocalSize", 8, 2, 2
+      }
+      hal.interface @io attributes {sym_visibility = "private"} {
+        hal.interface.binding @s0b0_ro_constant, set=0, binding=0, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b1_ro_external, set=0, binding=1, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b2_xw_external, set=0, binding=2, type="StorageBuffer", access="Write|Discard"
+      }
+    }
+  }
+}
+hal.executable @call_dispatch_4 attributes {sym_visibility = "private"} {
+  hal.interface @io {
+    hal.interface.binding @s0b0_ro_constant, set=0, binding=0, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b1_ro_external, set=0, binding=1, type="StorageBuffer", access="Read"
+    hal.interface.binding @s0b2_xw_external, set=0, binding=2, type="StorageBuffer", access="Write|Discard"
+  }
+  hal.executable.variant @vulkan_spirv_fb, target = #executable_target_vulkan_spirv_fb {
+    hal.executable.entry_point @call_dispatch_4 attributes {interface = @io, ordinal = 0 : index}
+    module {
+      spv.module Logical GLSL450 requires #spv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]> {
+        spv.func @call_dispatch_4() "None" {
+          spv.Return
+        }
+        spv.EntryPoint "GLCompute" @call_dispatch_4
+        spv.ExecutionMode @call_dispatch_4 "LocalSize", 2, 8, 1
+      }
+      hal.interface @io attributes {sym_visibility = "private"} {
+        hal.interface.binding @s0b0_ro_constant, set=0, binding=0, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b1_ro_external, set=0, binding=1, type="StorageBuffer", access="Read"
+        hal.interface.binding @s0b2_xw_external, set=0, binding=2, type="StorageBuffer", access="Write|Discard"
+      }
+    }
+  }
+}
+
+// Two groups should be created, according to their interfaces.
+
+//      CHECK: hal.executable @linking_linked_vulkan_0 {
+// CHECK-NEXT:   hal.interface @io_0 {
+// CHECK-NEXT:     hal.interface.binding @s0b0_ro_constant, set=0, binding=0, type="StorageBuffer", access="Read"
+// CHECK-NEXT:     hal.interface.binding @s0b1_ro_external, set=0, binding=1, type="StorageBuffer", access="Read"
+// CHECK-NEXT:     hal.interface.binding @s0b2_xw_external, set=0, binding=2, type="StorageBuffer", access="Write|Discard"
+// CHECK-NEXT:   }
+// CHECK-NEXT:   hal.executable.variant @vulkan_spirv_fb, target = #executable_target_vulkan_spirv_fb {
+// CHECK-NEXT:     hal.executable.entry_point @call_dispatch_1 attributes {interface = @io_0, ordinal = 0 : index}
+// CHECK-NEXT:     hal.executable.entry_point @call_dispatch_3 attributes {interface = @io_0, ordinal = 1 : index}
+// CHECK-NEXT:     hal.executable.entry_point @call_dispatch_4 attributes {interface = @io_0, ordinal = 2 : index}
+// CHECK-NEXT:     module  {
+// CHECK-NEXT:       spv.module Logical GLSL450 requires #spv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]> {
+// CHECK-NEXT:         spv.func @call_dispatch_1() "None" {
+// CHECK-NEXT:           spv.Return
+// CHECK-NEXT:         }
+// CHECK-NEXT:         spv.EntryPoint "GLCompute" @call_dispatch_1
+// CHECK-NEXT:         spv.ExecutionMode @call_dispatch_1 "LocalSize", 4, 4, 1
+// CHECK-NEXT:         spv.func @call_dispatch_3() "None" {
+// CHECK-NEXT:           spv.Return
+// CHECK-NEXT:         }
+// CHECK-NEXT:         spv.EntryPoint "GLCompute" @call_dispatch_3
+// CHECK-NEXT:         spv.ExecutionMode @call_dispatch_3 "LocalSize", 8, 2, 2
+// CHECK-NEXT:         spv.func @call_dispatch_4() "None" {
+// CHECK-NEXT:           spv.Return
+// CHECK-NEXT:         }
+// CHECK-NEXT:         spv.EntryPoint "GLCompute" @call_dispatch_4
+// CHECK-NEXT:         spv.ExecutionMode @call_dispatch_4 "LocalSize", 2, 8, 1
+// CHECK-NEXT:       }
+// CHECK-NEXT:     }
+// CHECK-NEXT:   }
+// CHECK-NEXT: }
+
+//      CHECK: hal.executable @linking_linked_vulkan {
+// CHECK-NEXT:   hal.interface @io_0 {
+// CHECK-NEXT:     hal.interface.binding @s0b0_ro_external, set=0, binding=0, type="StorageBuffer", access="Read"
+// CHECK-NEXT:     hal.interface.binding @s0b1_rw_external, set=0, binding=1, type="StorageBuffer", access="Read|Write"
+// CHECK-NEXT:   }
+// CHECK-NEXT:   hal.executable.variant @vulkan_spirv_fb, target = #executable_target_vulkan_spirv_fb {
+// CHECK-NEXT:     hal.executable.entry_point @call_dispatch_0 attributes {interface = @io_0, ordinal = 0 : index}
+// CHECK-NEXT:     hal.executable.entry_point @call_dispatch_2 attributes {interface = @io_0, ordinal = 1 : index}
+// CHECK-NEXT:     module  {
+// CHECK-NEXT:       spv.module Logical GLSL450 requires #spv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]> {
+// CHECK-NEXT:         spv.func @call_dispatch_0() "None" {
+// CHECK-NEXT:           spv.Return
+// CHECK-NEXT:         }
+// CHECK-NEXT:         spv.EntryPoint "GLCompute" @call_dispatch_0
+// CHECK-NEXT:         spv.ExecutionMode @call_dispatch_0 "LocalSize", 32, 1, 1
+// CHECK-NEXT:         spv.func @call_dispatch_2() "None" {
+// CHECK-NEXT:           spv.Return
+// CHECK-NEXT:         }
+// CHECK-NEXT:         spv.EntryPoint "GLCompute" @call_dispatch_2
+// CHECK-NEXT:         spv.ExecutionMode @call_dispatch_2 "LocalSize", 32, 1, 1
+// CHECK-NEXT:       }
+// CHECK-NEXT:     }
+// CHECK-NEXT:   }
+// CHECK-NEXT: }