[Codegen][Tuner] Add support for default tuning specs (#19394)

These default specs are target architecture-specific and will be shipped
with the compiler.

* Default specs belong to target plugins and get embedded in
`libIREECompiler.so`, just like ukernels.
* Plugins then register their default tuning specs with the default
embedded directory.
* We store them as mlir text. We can't easily assemble them as mlir
bytecode without taking a circular dependency on iree-opt. We can
revisit this in the future and add a new tool `iree-as` that will only
link with dialects.
* After the initial loading, we cache the default specs in the IREE
codegen dialect transform library manager.
* Add a placeholder spec for gfx942.
* Document and test the inclusion order. User specs come before default
specs.

Issue: https://github.com/iree-org/iree/issues/19214
diff --git a/compiler/plugins/target/ROCM/BUILD.bazel b/compiler/plugins/target/ROCM/BUILD.bazel
index 682806e..ef7aeec 100644
--- a/compiler/plugins/target/ROCM/BUILD.bazel
+++ b/compiler/plugins/target/ROCM/BUILD.bazel
@@ -27,6 +27,7 @@
         "ROCMTargetUtils.h",
     ],
     deps = [
+        "//compiler/plugins/target/ROCM/builtins/tuning:iree_default_tuning_specs_amdgpu",
         "//compiler/plugins/target/ROCM/builtins/ukernel:iree_uk_amdgpu_bitcode",
         "//compiler/src/iree/compiler/Codegen/Common",
         "//compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR:IREECodegenDialect",
diff --git a/compiler/plugins/target/ROCM/CMakeLists.txt b/compiler/plugins/target/ROCM/CMakeLists.txt
index 69204ab..a9f4dc1 100644
--- a/compiler/plugins/target/ROCM/CMakeLists.txt
+++ b/compiler/plugins/target/ROCM/CMakeLists.txt
@@ -65,6 +65,7 @@
     iree::compiler::Dialect::HAL::Utils::LLVMLinkerUtils
     iree::compiler::PluginAPI
     iree::compiler::Utils
+    iree::compiler::plugins::target::ROCM::builtins::tuning::iree_default_tuning_specs_amdgpu
     iree::compiler::plugins::target::ROCM::builtins::ukernel::iree_uk_amdgpu_bitcode
     iree::schemas::amdgpu_executable_def_c_fbs
     iree::schemas::executable_debug_info_c_fbs
diff --git a/compiler/plugins/target/ROCM/ROCMTarget.cpp b/compiler/plugins/target/ROCM/ROCMTarget.cpp
index 2036457..851b6ed 100644
--- a/compiler/plugins/target/ROCM/ROCMTarget.cpp
+++ b/compiler/plugins/target/ROCM/ROCMTarget.cpp
@@ -8,6 +8,7 @@
 
 #include <cstdint>
 
+#include "compiler/plugins/target/ROCM/builtins/tuning/iree_default_tuning_specs_amdgpu.h"
 #include "compiler/plugins/target/ROCM/builtins/ukernel/iree_uk_amdgpu_bitcode.h"
 #include "iree/compiler/Codegen/Common/Passes.h"
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.h"
@@ -900,14 +901,9 @@
   }
 };
 
-} // namespace
-
-} // namespace mlir::iree_compiler::IREE::HAL
-
 // Iterate over ukernel bitcode embedded-data files, and insert them into the
 // EmbeddedDataDirectory singleton.
 static void addAMDGPUUkernelBitcodeToGlobalEmbeddedDataDirectory() {
-  using mlir::iree_compiler::EmbeddedDataDirectory;
   EmbeddedDataDirectory::withGlobal([](EmbeddedDataDirectory &dir) {
     const iree_file_toc_t *toc = iree_uk_amdgpu_bitcode_create();
     for (size_t i = 0; i < iree_uk_amdgpu_bitcode_size(); ++i) {
@@ -916,11 +912,30 @@
   });
 }
 
+// Iterate over default tuning spec embedded-data files, and insert them into
+// the EmbeddedDataDirectory singleton.
+static void addAMDGPUDefaultTuningSpecsToGlobalEmbeddedDataDirectory() {
+  EmbeddedDataDirectory::withGlobal([](EmbeddedDataDirectory &dir) {
+    const iree_file_toc_t *toc = iree_default_tuning_specs_amdgpu_create();
+    for (size_t i = 0, e = iree_default_tuning_specs_amdgpu_size(); i != e;
+         ++i) {
+      dir.addFile(toc[i].name, llvm::StringRef{toc[i].data, toc[i].size});
+    }
+  });
+}
+
+} // namespace
+
+} // namespace mlir::iree_compiler::IREE::HAL
+
 extern "C" bool iree_register_compiler_plugin_hal_target_rocm(
     mlir::iree_compiler::PluginRegistrar *registrar) {
   registrar->registerPlugin<mlir::iree_compiler::IREE::HAL::ROCMSession>(
       "hal_target_rocm");
-  addAMDGPUUkernelBitcodeToGlobalEmbeddedDataDirectory();
+  mlir::iree_compiler::IREE::HAL::
+      addAMDGPUUkernelBitcodeToGlobalEmbeddedDataDirectory();
+  mlir::iree_compiler::IREE::HAL::
+      addAMDGPUDefaultTuningSpecsToGlobalEmbeddedDataDirectory();
   return true;
 }
 
diff --git a/compiler/plugins/target/ROCM/builtins/tuning/BUILD.bazel b/compiler/plugins/target/ROCM/builtins/tuning/BUILD.bazel
new file mode 100644
index 0000000..00fc739
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/tuning/BUILD.bazel
@@ -0,0 +1,53 @@
+# Copyright 2024 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+load("//build_tools/bazel:build_defs.oss.bzl", "iree_cmake_extra_content")
+load("//build_tools/bazel:iree_lit_test.bzl", "iree_lit_test_suite")
+load("//build_tools/embed_data:build_defs.bzl", "iree_c_embed_data")
+
+package(
+    default_visibility = ["//visibility:public"],
+    features = ["layering_check"],
+    licenses = ["notice"],  # Apache 2.0
+)
+
+iree_cmake_extra_content(
+    content = """
+if(NOT IREE_TARGET_BACKEND_ROCM)
+  return()
+endif()
+""",
+    inline = True,
+)
+
+# Target archs for tuning specs. https://llvm.org/docs/AMDGPUUsage.html#processors
+gpu_archs = [
+    "gfx942",
+]
+
+tuning_spec_mlir_files = [
+    "iree_default_tuning_spec_%s.mlir" % gpu_arch
+    for gpu_arch in gpu_archs
+]
+
+iree_c_embed_data(
+    name = "iree_default_tuning_specs_amdgpu",
+    srcs = tuning_spec_mlir_files,
+    c_file_output = "iree_default_tuning_specs_amdgpu.c",
+    flatten = True,
+    h_file_output = "iree_default_tuning_specs_amdgpu.h",
+)
+
+# Verify that the tuning specs are valid. We need this here because we do not
+# assemble the tuning spec mlir and invalid specs do not lead to build errors.
+iree_lit_test_suite(
+    name = "verify_default_tuning_specs_amdgpu",
+    srcs = tuning_spec_mlir_files,
+    cfg = "//compiler:lit.cfg.py",
+    tools = [
+        "//tools:iree-opt",
+    ],
+)
diff --git a/compiler/plugins/target/ROCM/builtins/tuning/CMakeLists.txt b/compiler/plugins/target/ROCM/builtins/tuning/CMakeLists.txt
new file mode 100644
index 0000000..a04c528
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/tuning/CMakeLists.txt
@@ -0,0 +1,39 @@
+################################################################################
+# Autogenerated by build_tools/bazel_to_cmake/bazel_to_cmake.py from           #
+# compiler/plugins/target/ROCM/builtins/tuning/BUILD.bazel                     #
+#                                                                              #
+# Use iree_cmake_extra_content from iree/build_defs.oss.bzl to add arbitrary   #
+# CMake-only content.                                                          #
+#                                                                              #
+# To disable autogeneration for this file entirely, delete this header.        #
+################################################################################
+
+iree_add_all_subdirs()
+
+if(NOT IREE_TARGET_BACKEND_ROCM)
+  return()
+endif()
+
+iree_c_embed_data(
+  NAME
+    iree_default_tuning_specs_amdgpu
+  SRCS
+    "iree_default_tuning_spec_gfx942.mlir"
+  C_FILE_OUTPUT
+    "iree_default_tuning_specs_amdgpu.c"
+  H_FILE_OUTPUT
+    "iree_default_tuning_specs_amdgpu.h"
+  FLATTEN
+  PUBLIC
+)
+
+iree_lit_test_suite(
+  NAME
+    verify_default_tuning_specs_amdgpu
+  SRCS
+    "iree_default_tuning_spec_gfx942.mlir"
+  TOOLS
+    iree-opt
+)
+
+### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###
diff --git a/compiler/plugins/target/ROCM/builtins/tuning/iree_default_tuning_spec_gfx942.mlir b/compiler/plugins/target/ROCM/builtins/tuning/iree_default_tuning_spec_gfx942.mlir
new file mode 100644
index 0000000..f0a8ca5
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/tuning/iree_default_tuning_spec_gfx942.mlir
@@ -0,0 +1,15 @@
+// RUN: iree-opt %s
+
+// This is just an initial tuning spec for gfx942 and is not intended for
+// production use.
+// TODO(https://github.com/iree-org/iree/issues/19214): Add missing
+// configurations to this spec.
+
+module @iree_default_tuning_spec_gfx942 attributes { transform.with_named_sequence } {
+
+transform.named_sequence @__kernel_config(%variant_op: !transform.any_op {transform.readonly}) -> ()
+  attributes { iree_codegen.tuning_spec_entrypoint } {
+  transform.yield
+}
+
+}
diff --git a/compiler/plugins/target/ROCM/builtins/tuning/test/BUILD.bazel b/compiler/plugins/target/ROCM/builtins/tuning/test/BUILD.bazel
new file mode 100644
index 0000000..ab6ada2
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/tuning/test/BUILD.bazel
@@ -0,0 +1,35 @@
+# Copyright 2024 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+load("//build_tools/bazel:build_defs.oss.bzl", "iree_cmake_extra_content")
+load("//build_tools/bazel:iree_lit_test.bzl", "iree_lit_test_suite")
+
+package(
+    default_visibility = ["//visibility:public"],
+    features = ["layering_check"],
+    licenses = ["notice"],  # Apache 2.0
+)
+
+iree_cmake_extra_content(
+    content = """
+if(NOT IREE_TARGET_BACKEND_ROCM)
+  return()
+endif()
+""",
+    inline = True,
+)
+
+iree_lit_test_suite(
+    name = "lit",
+    srcs = [
+        "spec_gfx942.mlir",
+    ],
+    cfg = "//compiler:lit.cfg.py",
+    tools = [
+        "//tools:iree-opt",
+        "@llvm-project//llvm:FileCheck",
+    ],
+)
diff --git a/compiler/plugins/target/ROCM/builtins/tuning/test/CMakeLists.txt b/compiler/plugins/target/ROCM/builtins/tuning/test/CMakeLists.txt
new file mode 100644
index 0000000..0ed3614
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/tuning/test/CMakeLists.txt
@@ -0,0 +1,27 @@
+################################################################################
+# Autogenerated by build_tools/bazel_to_cmake/bazel_to_cmake.py from           #
+# compiler/plugins/target/ROCM/builtins/tuning/test/BUILD.bazel                #
+#                                                                              #
+# Use iree_cmake_extra_content from iree/build_defs.oss.bzl to add arbitrary   #
+# CMake-only content.                                                          #
+#                                                                              #
+# To disable autogeneration for this file entirely, delete this header.        #
+################################################################################
+
+iree_add_all_subdirs()
+
+if(NOT IREE_TARGET_BACKEND_ROCM)
+  return()
+endif()
+
+iree_lit_test_suite(
+  NAME
+    lit
+  SRCS
+    "spec_gfx942.mlir"
+  TOOLS
+    FileCheck
+    iree-opt
+)
+
+### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###
diff --git a/compiler/plugins/target/ROCM/builtins/tuning/test/spec_gfx942.mlir b/compiler/plugins/target/ROCM/builtins/tuning/test/spec_gfx942.mlir
new file mode 100644
index 0000000..51c0e01
--- /dev/null
+++ b/compiler/plugins/target/ROCM/builtins/tuning/test/spec_gfx942.mlir
@@ -0,0 +1,28 @@
+// RUN: iree-opt --split-input-file --iree-gpu-test-target=gfx942 \
+// RUN:   --pass-pipeline="builtin.module(hal.executable(hal.executable.variant(iree-hal-configure-target-executable-variants{target=rocm})))" \
+// RUN:   --iree-codegen-enable-default-tuning-specs \
+// RUN:   --iree-codegen-notify-transform-strategy-application \
+// RUN:   --verify-diagnostics %s | FileCheck %s
+
+// CHECK-LABEL:      func.func @placeholder
+
+#pipeline_layout = #hal.pipeline.layout<bindings = [
+  #hal.pipeline.binding<storage_buffer>,
+  #hal.pipeline.binding<storage_buffer>,
+  #hal.pipeline.binding<storage_buffer>
+]>
+hal.executable public @main {
+  hal.executable.variant public @rocm_hsaco_fb target(<"rocm", "rocm-hsaco-fb">) {
+    hal.executable.export public @placeholder ordinal(0) layout(#pipeline_layout) {
+    ^bb0(%arg0: !hal.device):
+      %x, %y, %z = flow.dispatch.workgroup_count_from_slice
+      hal.return %x, %y, %z : index, index, index
+    }
+    builtin.module {
+      // expected-remark@+1 {{Applied transform configuration strategy @iree_default_tuning_spec_gfx942::@__kernel_config}}
+      func.func @placeholder() {
+        return
+      }
+    }
+  }
+}
diff --git a/compiler/plugins/target/ROCM/test/BUILD.bazel b/compiler/plugins/target/ROCM/test/BUILD.bazel
index ebf4dfd..f0521a0 100644
--- a/compiler/plugins/target/ROCM/test/BUILD.bazel
+++ b/compiler/plugins/target/ROCM/test/BUILD.bazel
@@ -15,6 +15,7 @@
 iree_lit_test_suite(
     name = "lit",
     srcs = [
+        "default_tuning_specs_amdgpu.mlir",
         "gpu_lower_to_ukernels.mlir",
         "lowering_strategy_from_tuning_spec.mlir",
         "ukernel_pipeline_transform.mlir",
diff --git a/compiler/plugins/target/ROCM/test/CMakeLists.txt b/compiler/plugins/target/ROCM/test/CMakeLists.txt
index 38158aa..36d9ba6 100644
--- a/compiler/plugins/target/ROCM/test/CMakeLists.txt
+++ b/compiler/plugins/target/ROCM/test/CMakeLists.txt
@@ -14,6 +14,7 @@
   NAME
     lit
   SRCS
+    "default_tuning_specs_amdgpu.mlir"
     "gpu_lower_to_ukernels.mlir"
     "lowering_strategy_from_tuning_spec.mlir"
     "ukernel_pipeline_transform.mlir"
diff --git a/compiler/plugins/target/ROCM/test/default_tuning_specs_amdgpu.mlir b/compiler/plugins/target/ROCM/test/default_tuning_specs_amdgpu.mlir
new file mode 100644
index 0000000..f049f79
--- /dev/null
+++ b/compiler/plugins/target/ROCM/test/default_tuning_specs_amdgpu.mlir
@@ -0,0 +1,55 @@
+// RUN: iree-opt --pass-pipeline='builtin.module(iree-codegen-materialize-tuning-specs)' \
+// RUN:   --iree-codegen-enable-default-tuning-specs \
+// RUN:   --iree-codegen-dump-tuning-specs-to=- \
+// RUN:   --iree-gpu-test-target=gfx942 --mlir-disable-threading \
+// RUN:   --no-implicit-module %s | FileCheck %s --check-prefix=DEFAULT
+
+// RUN: iree-opt --pass-pipeline='builtin.module(iree-codegen-materialize-tuning-specs)' \
+// RUN:   --iree-codegen-tuning-spec-path=%p/tuning_spec_mmt_tile_and_fuse.mlir \
+// RUN:   --iree-codegen-enable-default-tuning-specs \
+// RUN:   --iree-codegen-dump-tuning-specs-to=- \
+// RUN:   --iree-gpu-test-target=gfx942 --mlir-disable-threading \
+// RUN:   --no-implicit-module %s | FileCheck %s --check-prefix=BOTH
+
+// Note: This test needs to be in the plugin subdirectory because it depends
+// on the default spec that's only embedded in the compiler library when the
+// ROCM plugin is built.
+
+// ============================================================================
+
+// Check that the default tuning spec gets materialized without linking.
+
+// DEFAULT-LABEL: module @iree_default_tuning_spec_gfx942 attributes {transform.with_named_sequence}
+// DEFAULT-LABEL:   transform.named_sequence @__kernel_config
+// DEFAULT-SAME:      attributes {iree_codegen.tuning_spec_entrypoint}
+
+// Check that the default tuning spec gets materialized as a module attribute.
+// DEFAULT:        module attributes
+// DEFAULT-SAME:     iree_codegen.tuning_spec_mlirbc = dense<{{.+}}> : vector<{{[0-9]+}}xi8>
+// DEFAULT-LABEL:    func.func @main_0
+
+// ============================================================================
+
+// Check that both the user tuning spec and the default spec get linked and
+// materialized. The user spec should have precedence over the default one.
+
+// BOTH-LABEL: module @iree_linked_tuning_spec attributes {transform.with_named_sequence}
+// BOTH-LABEL:   module @mmt_tile_and_fuse_spec_0 attributes {transform.with_named_sequence}
+// BOTH-LABEL:     transform.named_sequence @main
+// BOTH-SAME:        attributes {iree_codegen.tuning_spec_entrypoint}
+// BOTH-LABEL:   module @iree_default_tuning_spec_gfx942_1 attributes {transform.with_named_sequence}
+// BOTH:           transform.named_sequence @__kernel_config
+// BOTH-SAME:        attributes {iree_codegen.tuning_spec_entrypoint}
+// BOTH:         transform.named_sequence @__kernel_config
+// BOTH:           @mmt_tile_and_fuse_spec_0::@main
+// BOTH:           @iree_default_tuning_spec_gfx942_1::@__kernel_config
+
+// BOTH:        module attributes
+// BOTH-SAME:     iree_codegen.tuning_spec_mlirbc = dense<{{.+}}> : vector<{{[0-9]+}}xi8>
+// BOTH-LABEL:    func.func @main_0
+
+module {
+  func.func @main_0() {
+    return
+  }
+}
diff --git a/compiler/src/iree/compiler/Codegen/Common/MaterializeTuningSpecsPass.cpp b/compiler/src/iree/compiler/Codegen/Common/MaterializeTuningSpecsPass.cpp
index f14aa92..7495f43 100644
--- a/compiler/src/iree/compiler/Codegen/Common/MaterializeTuningSpecsPass.cpp
+++ b/compiler/src/iree/compiler/Codegen/Common/MaterializeTuningSpecsPass.cpp
@@ -8,14 +8,19 @@
 #include "iree/compiler/Codegen/Common/Passes.h"
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenAttrs.h"
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.h"
+#include "iree/compiler/Codegen/Utils/GPUUtils.h"
+#include "iree/compiler/Utils/EmbeddedDataDirectory.h"
 #include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/SmallVectorExtras.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/ToolOutputFile.h"
 #include "llvm/Support/raw_ostream.h"
 #include "mlir/Bytecode/BytecodeWriter.h"
 #include "mlir/Dialect/Transform/IR/TransformDialect.h"
 #include "mlir/Dialect/Transform/IR/TransformOps.h"
 #include "mlir/Dialect/Transform/IR/TransformTypes.h"
+#include "mlir/IR/AsmState.h"
 #include "mlir/IR/Builders.h"
 #include "mlir/IR/BuiltinAttributes.h"
 #include "mlir/IR/BuiltinOps.h"
@@ -41,6 +46,12 @@
                    "dialect library)."),
     llvm::cl::init(""));
 
+llvm::cl::opt<bool> clCodegenEnableDefaultTuningSpecs(
+    "iree-codegen-enable-default-tuning-specs",
+    llvm::cl::desc("Whether to enable default tuning spec transform libraries "
+                   "shipped with the compiler"),
+    llvm::cl::init(false));
+
 llvm::cl::opt<std::string> clCodegenTuningSpecDumpDir(
     "iree-codegen-dump-tuning-specs-to",
     llvm::cl::desc(
@@ -50,8 +61,11 @@
 
 using mlir::transform::NamedSequenceOp;
 
-static LogicalResult dumpFinalTuningSpecToDir(ModuleOp tuningSpec,
-                                              StringRef dir) {
+static LogicalResult dumpFinalTuningSpecToDir(ModuleOp tuningSpec) {
+  StringRef dir = clCodegenTuningSpecDumpDir;
+  if (dir.empty()) {
+    return success();
+  }
   if (dir == "-") {
     tuningSpec->print(llvm::outs());
     return success();
@@ -79,6 +93,55 @@
   return success();
 }
 
+static FailureOr<ModuleOp>
+getUserTuningSpec(ModuleOp module, IREE::Codegen::IREECodegenDialect &dialect) {
+  if (clCodegenTuningSpecPath.empty()) {
+    return failure();
+  }
+
+  FailureOr<ModuleOp> maybeTransformLibrary =
+      dialect.getOrLoadTransformLibraryModule(clCodegenTuningSpecPath);
+  if (failed(maybeTransformLibrary)) {
+    return module->emitError()
+           << "Failed to load tuning spec transform dialect library from "
+           << clCodegenTuningSpecPath;
+  }
+
+  return *maybeTransformLibrary;
+}
+
+static FailureOr<ModuleOp>
+getDefaultTuningSpec(ModuleOp module,
+                     IREE::Codegen::IREECodegenDialect &dialect) {
+  if (!clCodegenEnableDefaultTuningSpecs) {
+    return failure();
+  }
+
+  IREE::GPU::TargetAttr gpuTarget = getGPUTargetAttr(module);
+  if (!gpuTarget) {
+    return failure();
+  }
+
+  // Try to look up the default tuning spec for this architecture, if any.
+  StringRef arch = gpuTarget.getArch();
+  std::string defaultTuningSpecName =
+      llvm::formatv("iree_default_tuning_spec_{}.mlir", arch);
+  std::optional<StringRef> defaultTuningSpecSource;
+  EmbeddedDataDirectory::withGlobal([&](EmbeddedDataDirectory &dir) {
+    defaultTuningSpecSource = dir.getFile(defaultTuningSpecName);
+  });
+  if (!defaultTuningSpecSource) {
+    // Not all architectures are expected to provide default tuning specs, so
+    // this shouldn't be considered a hard error (but that's up to the caller).
+    return failure();
+  }
+
+  // Load the library through the codegen dialect so that we cache the parsed
+  // module.
+  return dialect.getOrParseTransformLibraryModule(defaultTuningSpecName,
+                                                  *defaultTuningSpecSource);
+}
+
 static FailureOr<DenseElementsAttr>
 serializeTuningSpecToAttr(ModuleOp tuningSpec) {
   std::string buffer;
@@ -101,29 +164,55 @@
   }
 
   void runOnOperation() override {
-    if (clCodegenTuningSpecPath.empty()) {
-      return;
-    }
-
     ModuleOp module = getOperation();
     MLIRContext *ctx = &getContext();
     auto dialect = ctx->getOrLoadDialect<IREE::Codegen::IREECodegenDialect>();
-    auto maybeTransformLibrary =
-        dialect->getOrLoadTransformLibraryModule(clCodegenTuningSpecPath);
-    if (failed(maybeTransformLibrary)) {
-      module->emitError()
-          << "Failed to load tuning spec transform dialect library from "
-          << clCodegenTuningSpecPath;
+    assert(dialect);
+
+    FailureOr<ModuleOp> userTuningSpec = getUserTuningSpec(module, *dialect);
+    const bool hasUserTuningSpec = succeeded(userTuningSpec);
+    if (!hasUserTuningSpec && !clCodegenTuningSpecPath.empty()) {
+      // When a user spec is requested but fails to load, this is a hard
+      // failure.
       return signalPassFailure();
     }
 
-    ModuleOp userTuningSpec = *maybeTransformLibrary;
-    if (!userTuningSpec.getSymName()) {
-      // Set a module name so that we can refer to its nested symbols.
-      userTuningSpec.setSymName("iree_user_tuning_spec");
+    FailureOr<ModuleOp> defaultTuningSpec =
+        getDefaultTuningSpec(module, *dialect);
+    const bool hasDefaultTuningSpec = succeeded(defaultTuningSpec);
+    if (!hasUserTuningSpec && !hasDefaultTuningSpec) {
+      // No specs available, nothing to do.
+      return;
     }
 
-    Location loc = userTuningSpec.getLoc();
+    // If only the default tuning spec is available, use it directly and skip
+    // the linking stage.
+    if (!hasUserTuningSpec) {
+      if (failed(dumpFinalTuningSpecToDir(*defaultTuningSpec))) {
+        return signalPassFailure();
+      }
+      FailureOr<DenseElementsAttr> serializedSpec =
+          serializeTuningSpecToAttr(*defaultTuningSpec);
+      if (failed(serializedSpec)) {
+        module->emitError("Failed to serialize default tuning specs");
+        return signalPassFailure();
+      }
+      module->setAttr(kSerializedTuningSpecAttrName, *serializedSpec);
+      return;
+    }
+
+    // When the user tuning spec is available, link all available libraries into
+    // a single module. We insert the default tuning spec last, so that any
+    // user-specified tuning configurations take precedence.
+    SmallVector<ModuleOp, 2> allSpecs = {*userTuningSpec};
+    if (hasDefaultTuningSpec) {
+      allSpecs.push_back(*defaultTuningSpec);
+    }
+
+    Location loc =
+        FusedLoc::get(ctx, llvm::map_to_vector<2>(allSpecs, [](ModuleOp m) {
+                        return m.getLoc();
+                      }));
 
     // This module will always be released at the end of the pass.
     OwningOpRef<ModuleOp> linkedTuningSpec(
@@ -131,7 +220,13 @@
     linkedTuningSpec.get()->setAttr(
         transform::TransformDialect::kWithNamedSequenceAttrName,
         UnitAttr::get(ctx));
-    linkedTuningSpec->insert(linkedTuningSpec->begin(), userTuningSpec.clone());
+    for (auto [idx, spec] : llvm::enumerate(allSpecs)) {
+      ModuleOp clonedSpec = spec.clone();
+      // Make sure there are no symbol name collisions.
+      clonedSpec.setSymName(
+          llvm::formatv("{}_{}", clonedSpec.getSymName().value(), idx).str());
+      linkedTuningSpec->push_back(clonedSpec);
+    }
 
     // TODO(https://github.com/iree-org/iree/issues/19214): Add linked tuning
     // spec memoization to IREECodegenDialect. We should be able to provide a
@@ -145,11 +240,8 @@
       return signalPassFailure();
     }
 
-    if (!clCodegenTuningSpecDumpDir.empty()) {
-      if (failed(dumpFinalTuningSpecToDir(linkedTuningSpec.get(),
-                                          clCodegenTuningSpecDumpDir))) {
-        return signalPassFailure();
-      }
+    if (failed(dumpFinalTuningSpecToDir(linkedTuningSpec.get()))) {
+      return signalPassFailure();
     }
 
     FailureOr<DenseElementsAttr> serializedSpec =
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/BUILD.bazel b/compiler/src/iree/compiler/Codegen/Common/test/BUILD.bazel
index 5644f48..5de2e3d 100644
--- a/compiler/src/iree/compiler/Codegen/Common/test/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/Common/test/BUILD.bazel
@@ -56,6 +56,7 @@
             "lower_ukernel_to_calls.mlir",
             "materialize_encoding_into_nop.mlir",
             "materialize_tuning_specs.mlir",
+            "materialize_tuning_specs_default_missing.mlir",
             "materialize_tuning_specs_invalid_spec.mlir",
             "materialize_user_config_from_tuning_spec.mlir",
             "materialize_user_configs.mlir",
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/Common/test/CMakeLists.txt
index 325e728..4dc774c 100644
--- a/compiler/src/iree/compiler/Codegen/Common/test/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/Common/test/CMakeLists.txt
@@ -52,6 +52,7 @@
     "lower_ukernel_to_calls.mlir"
     "materialize_encoding_into_nop.mlir"
     "materialize_tuning_specs.mlir"
+    "materialize_tuning_specs_default_missing.mlir"
     "materialize_tuning_specs_invalid_spec.mlir"
     "materialize_user_config_from_tuning_spec.mlir"
     "materialize_user_configs.mlir"
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/materialize_tuning_specs.mlir b/compiler/src/iree/compiler/Codegen/Common/test/materialize_tuning_specs.mlir
index d28cd68..8e2a081 100644
--- a/compiler/src/iree/compiler/Codegen/Common/test/materialize_tuning_specs.mlir
+++ b/compiler/src/iree/compiler/Codegen/Common/test/materialize_tuning_specs.mlir
@@ -3,13 +3,14 @@
 // RUN:   --iree-codegen-dump-tuning-specs-to=- \
 // RUN:   --mlir-disable-threading --no-implicit-module %s | FileCheck %s
 
-// Check that the final tuning spec is as expected.
+// Check that the final tuning spec is as expected when the user tuning spec is provided.
+
 // CHECK-LABEL: module @iree_linked_tuning_spec attributes {transform.with_named_sequence}
-// CHECK-LABEL:   module @user_spec attributes {transform.with_named_sequence}
+// CHECK-LABEL:   module @user_spec_0 attributes {transform.with_named_sequence}
 // CHECK-LABEL:     transform.named_sequence @hello
 // CHECK-SAME:        attributes {iree_codegen.tuning_spec_entrypoint}
 // CHECK-LABEL:   transform.named_sequence @__kernel_config
-// CHECK:           @user_spec::@hello
+// CHECK:           @user_spec_0::@hello
 
 // Check that the transform spec gets materialized as a module attribute.
 // CHECK:        module attributes
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/materialize_tuning_specs_default_missing.mlir b/compiler/src/iree/compiler/Codegen/Common/test/materialize_tuning_specs_default_missing.mlir
new file mode 100644
index 0000000..424dacd
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/Common/test/materialize_tuning_specs_default_missing.mlir
@@ -0,0 +1,14 @@
+// RUN: iree-opt --pass-pipeline='builtin.module(iree-codegen-materialize-tuning-specs)' \
+// RUN:   --iree-codegen-enable-default-tuning-specs --no-implicit-module %s \
+// RUN:   | FileCheck %s
+
+// Check that we do not materialize any tuning spec when there's no default spec for the given
+// target (since we do not set any target)
+// CHECK:        module {
+// CHECK-LABEL:    func.func @main_0
+
+module {
+  func.func @main_0() {
+    return
+  }
+}
diff --git a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.td b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.td
index 9e5a408..35775d0 100644
--- a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.td
+++ b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.td
@@ -36,8 +36,23 @@
   let extraClassDeclaration = [{
     void initializeCodegenAttrs();
 
+    /// Returns the loaded module for the `libraryPath` transform dialect
+    /// library MLIR input. If the module has already been loaded in the
+    /// past, returns the memoized module without parsing the file again.
+    /// This function is thead-safe.
     FailureOr<::mlir::ModuleOp>
-    getOrLoadTransformLibraryModule(std::string libraryPath);
+    getOrLoadTransformLibraryModule(StringRef libraryPath);
+
+    /// Returns the parsed module for the `libraryPath` transform dialect
+    /// library MLIR input `libraryMLIRSource`. If the module has already
+    /// been parsed in the past, returns the memoized module without parsing
+    /// the source again. Note that this function does not access
+    /// `libraryPath` and only uses it as the key associated with the
+    /// matching parsed module.
+    /// This function is thead-safe.
+    FailureOr<::mlir::ModuleOp>
+    getOrParseTransformLibraryModule(StringRef libraryPath,
+                                     StringRef libraryMLIRSource);
 
     private:
 
diff --git a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenLibraryManager.cpp b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenLibraryManager.cpp
index 437a80b..7380230 100644
--- a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenLibraryManager.cpp
+++ b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenLibraryManager.cpp
@@ -7,13 +7,16 @@
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenDialect.h"
 #include "mlir/Dialect/Transform/IR/TransformDialect.h"
 #include "mlir/Dialect/Transform/Transforms/TransformInterpreterUtils.h"
+#include "mlir/Parser/Parser.h"
 
 namespace mlir::iree_compiler::IREE::Codegen {
 
-FailureOr<ModuleOp>
-IREECodegenDialect::getOrLoadTransformLibraryModule(std::string libraryPath) {
-  // Acquire a lock on the map that will release once out of scope.
-  std::lock_guard<std::mutex> guard(libraryMutex);
+/// Helper function that implements the module lookup and validation.
+/// The caller is responsible for acquiring the mutex for `libraryModules`.
+static FailureOr<ModuleOp> getOrParseTransformLibraryModuleImpl(
+    StringRef libraryPath,
+    llvm::StringMap<OwningOpRef<ModuleOp>> &libraryModules,
+    llvm::function_ref<LogicalResult(OwningOpRef<ModuleOp> &)> loadModuleFn) {
 
   auto loadedLibrary = libraryModules.find(libraryPath);
   if (loadedLibrary != libraryModules.end()) {
@@ -28,9 +31,7 @@
   // succeeds so that other threads don't have to retry.
   OwningOpRef<ModuleOp> &parsedLibrary = libraryModules[libraryPath];
 
-  MLIRContext *ctx = getContext();
-  if (failed(transform::detail::parseTransformModuleFromFile(ctx, libraryPath,
-                                                             parsedLibrary))) {
+  if (failed(loadModuleFn(parsedLibrary))) {
     return failure();
   }
 
@@ -54,4 +55,32 @@
   return parsedLibrary.get();
 }
 
+FailureOr<ModuleOp>
+IREECodegenDialect::getOrLoadTransformLibraryModule(StringRef libraryPath) {
+  // Acquire a lock on the map that will release once out of scope.
+  std::lock_guard<std::mutex> guard(libraryMutex);
+  MLIRContext *ctx = getContext();
+
+  return getOrParseTransformLibraryModuleImpl(
+      libraryPath, libraryModules, [=](OwningOpRef<ModuleOp> &parsedLibrary) {
+        return transform::detail::parseTransformModuleFromFile(ctx, libraryPath,
+                                                               parsedLibrary);
+      });
+}
+
+FailureOr<ModuleOp> IREECodegenDialect::getOrParseTransformLibraryModule(
+    StringRef libraryPath, StringRef libraryMLIRSource) {
+  // Acquire a lock on the map that will release once out of scope.
+  std::lock_guard<std::mutex> guard(libraryMutex);
+  MLIRContext *ctx = getContext();
+
+  return getOrParseTransformLibraryModuleImpl(
+      libraryPath, libraryModules, [=](OwningOpRef<ModuleOp> &parsedLibrary) {
+        ParserConfig config(ctx);
+        parsedLibrary =
+            parseSourceString<ModuleOp>(libraryMLIRSource, ctx, libraryPath);
+        return success(*parsedLibrary != nullptr);
+      });
+}
+
 } // namespace mlir::iree_compiler::IREE::Codegen