[clang] [CIR] Add offload container attribute (PR #206576)
David Rivera via cfe-commits
cfe-commits at lists.llvm.org
Mon Sep 7 13:55:42 PDT 2026
https://github.com/RiverDave updated https://github.com/llvm/llvm-project/pull/206576
>From be1086b6453cc16402913c60f145a39abe7b3630 Mon Sep 17 00:00:00 2001
From: David Rivera <davidriverg at gmail.com>
Date: Mon, 29 Jun 2026 15:46:11 -0400
Subject: [PATCH 1/5] [CIR] Add offload container operation
Introduce cir.offload.container, a CIR dialect operation that groups one host CIR module with one or more device CIR modules in a single IR unit.
Nested modules are tagged with the new cir.offload.kind enum attribute using #cir.offload_kind<host> and #cir.offload_kind<device>. The verifier enforces the structural contract expected by follow-up offload merge/split pipeline patches: host module first, device modules after it, only nested builtin.module ops, and at least one device module.
This patch only adds the IR representation and verifier tests; the passes that create, consume, or split the container are left to later patches.
---
.../clang/CIR/Dialect/IR/CIRDialect.td | 1 +
clang/include/clang/CIR/Dialect/IR/CIROps.td | 59 ++++++++++++++++++
clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 61 +++++++++++++++++++
.../test/CIR/IR/invalid-offload-container.cir | 54 ++++++++++++++++
clang/test/CIR/IR/offload-container.cir | 32 ++++++++++
5 files changed, 207 insertions(+)
create mode 100644 clang/test/CIR/IR/invalid-offload-container.cir
create mode 100644 clang/test/CIR/IR/offload-container.cir
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
index 9ea186489ac65..00c8cfe008408 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
@@ -53,6 +53,7 @@ def CIR_Dialect : Dialect {
static llvm::StringRef getModuleLevelAsmAttrName() { return "cir.module_asm"; }
static llvm::StringRef getGlobalCtorsAttrName() { return "cir.global_ctors"; }
static llvm::StringRef getGlobalDtorsAttrName() { return "cir.global_dtors"; }
+ static llvm::StringRef getOffloadKindAttrName() { return "cir.offload.kind"; }
static llvm::StringRef getOperandSegmentSizesAttrName() { return "operandSegmentSizes"; }
static llvm::StringRef getNoCallerSavedRegsAttrName() { return "no_caller_saved_registers"; }
static llvm::StringRef getNoCallbackAttrName() { return "nocallback"; }
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 6d149e6b1ccaa..158b97adfbe4e 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -6141,6 +6141,65 @@ def CIR_VecSplatOp : CIR_Op<"vec.splat", [
}];
}
+//===----------------------------------------------------------------------===//
+// OffloadKind
+//===----------------------------------------------------------------------===//
+
+def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
+ I32EnumAttrCase<"Host", 0, "host">,
+ I32EnumAttrCase<"Device", 1, "device">
+]> {
+ let genSpecializedAttr = 0;
+}
+
+def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
+ let summary = "Offload kind (host or device)";
+}
+
+//===----------------------------------------------------------------------===//
+// OffloadContainerOp
+//===----------------------------------------------------------------------===//
+
+def CIR_OffloadContainerOp : CIR_Op<"offload.container", [
+ NoRegionArguments, NoTerminator, SingleBlock, SymbolTable]> {
+ let summary = "Container for host and device CIR modules";
+ let description = [{
+ `cir.offload.container` groups one host CIR module with one or more device
+ CIR modules for offload-aware analysis and transformation.
+
+ The body holds nested `builtin.module` operations. The first nested
+ module is the host module and must carry
+ `cir.offload.kind = #cir.offload_kind<host>`. All remaining nested
+ modules are device modules and must carry
+ `cir.offload.kind = #cir.offload_kind<device>`. There must be at least
+ one device module.
+
+ Example:
+
+ ```mlir
+ cir.offload.container {
+ builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+ }
+ }
+ ```
+ }];
+
+ let regions = (region SizedRegion<1>:$body);
+
+ let assemblyFormat = "$body attr-dict";
+
+ let hasVerifier = 1;
+ let hasLLVMLowering = false;
+
+ let extraClassDeclaration = [{
+ mlir::ModuleOp getHostModule();
+ llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+ getDeviceModules();
+ }];
+}
+
//===----------------------------------------------------------------------===//
// BaseClassAddrOp
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 606b4fd9d3fa6..b36424aa31ded 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -18,6 +18,7 @@
#include "mlir/IR/Attributes.h"
#include "mlir/IR/BuiltinTypes.h"
+#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/DialectImplementation.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/IR/Value.h"
@@ -2374,6 +2375,66 @@ LogicalResult cir::VTTAddrPointOp::verify() {
return success();
}
+//===----------------------------------------------------------------------===//
+// OffloadContainerOp
+//===----------------------------------------------------------------------===//
+
+mlir::ModuleOp cir::OffloadContainerOp::getHostModule() {
+ return mlir::cast<mlir::ModuleOp>(getBody().front().front());
+}
+
+llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+cir::OffloadContainerOp::getDeviceModules() {
+ mlir::Block &body = getBody().front();
+ auto begin = body.op_begin<mlir::ModuleOp>();
+ auto end = body.op_end<mlir::ModuleOp>();
+ if (begin != end)
+ ++begin;
+ return {begin, end};
+}
+
+static LogicalResult checkOffloadKind(mlir::ModuleOp module,
+ cir::OffloadKind expected) {
+ auto attr = module->getAttrOfType<cir::OffloadKindAttr>(
+ cir::CIRDialect::getOffloadKindAttrName());
+ if (!attr)
+ return module.emitOpError()
+ << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+ << "' offload kind attribute";
+ if (attr.getValue() != expected)
+ return module.emitOpError()
+ << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+ << "' value '" << cir::stringifyOffloadKind(expected) << "'";
+ return success();
+}
+
+LogicalResult cir::OffloadContainerOp::verify() {
+ mlir::Block &body = getBody().front();
+ if (body.empty())
+ return emitOpError() << "expects host module as the first nested op";
+
+ auto host = mlir::dyn_cast<mlir::ModuleOp>(body.front());
+ if (!host)
+ return emitOpError() << "expects host module as the first nested op";
+ if (failed(checkOffloadKind(host, cir::OffloadKind::Host)))
+ return failure();
+
+ unsigned numDevices = 0;
+ auto it = body.begin();
+ for (++it; it != body.end(); ++it) {
+ auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
+ if (!module)
+ return emitOpError() << "expects only nested builtin.module ops";
+ if (failed(checkOffloadKind(module, cir::OffloadKind::Device)))
+ return failure();
+ ++numDevices;
+ }
+
+ if (numDevices == 0)
+ return emitOpError() << "expects at least one device module";
+ return success();
+}
+
//===----------------------------------------------------------------------===//
// FuncOp
//===----------------------------------------------------------------------===//
diff --git a/clang/test/CIR/IR/invalid-offload-container.cir b/clang/test/CIR/IR/invalid-offload-container.cir
new file mode 100644
index 0000000000000..219442f39674a
--- /dev/null
+++ b/clang/test/CIR/IR/invalid-offload-container.cir
@@ -0,0 +1,54 @@
+// RUN: cir-opt %s -verify-diagnostics -split-input-file
+
+module {
+ cir.offload.container {
+ builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} { // expected-error {{expects 'cir.offload.kind' value 'host'}}
+ }
+ }
+}
+
+// -----
+
+module {
+ cir.offload.container {
+ builtin.module @host_0 attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ builtin.module @host_1 attributes {cir.offload.kind = #cir.offload_kind<host>} { // expected-error {{expects 'cir.offload.kind' value 'device'}}
+ }
+ }
+}
+
+// -----
+
+module {
+ cir.offload.container {
+ builtin.module @host { // expected-error {{expects 'cir.offload.kind' offload kind attribute}}
+ }
+ }
+}
+
+// -----
+
+module {
+ cir.offload.container { // expected-error {{expects only nested builtin.module ops}}
+ builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ cir.const #cir.int<0> : !cir.int<s, 32>
+ }
+}
+
+// -----
+
+module {
+ cir.offload.container { // expected-error {{expects at least one device module}}
+ builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ }
+}
+
+// -----
+
+module {
+ cir.offload.container { // expected-error {{expects host module as the first nested op}}
+ }
+}
diff --git a/clang/test/CIR/IR/offload-container.cir b/clang/test/CIR/IR/offload-container.cir
new file mode 100644
index 0000000000000..b7b53aa973ce5
--- /dev/null
+++ b/clang/test/CIR/IR/offload-container.cir
@@ -0,0 +1,32 @@
+// RUN: cir-opt %s -split-input-file --verify-roundtrip | FileCheck %s
+
+module {
+ cir.offload.container {
+ builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+ }
+ }
+}
+
+// CHECK: cir.offload.container {
+// CHECK: builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+// CHECK: builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+
+// -----
+
+module {
+ cir.offload.container {
+ builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+ }
+ builtin.module @device_1 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+ }
+ }
+}
+
+// CHECK: cir.offload.container {
+// CHECK: builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+// CHECK: builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+// CHECK: builtin.module @device_1 attributes {cir.offload.kind = #cir.offload_kind<device>} {
>From c728fb4c32ae57382a1f8903bed2a6ef1b20ff8d Mon Sep 17 00:00:00 2001
From: David Rivera <davidriverg at gmail.com>
Date: Mon, 29 Jun 2026 16:00:27 -0400
Subject: [PATCH 2/5] fix fmt
---
clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index b36424aa31ded..5415e9285801d 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -17,8 +17,8 @@
#include "clang/CIR/Dialect/IR/CIRTypes.h"
#include "mlir/IR/Attributes.h"
-#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/BuiltinOps.h"
+#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/DialectImplementation.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/IR/Value.h"
>From 4054988194f0af15917f36936cbd6f686b8dcaf4 Mon Sep 17 00:00:00 2001
From: David Rivera <davidriverg at gmail.com>
Date: Fri, 14 Aug 2026 08:11:59 -0400
Subject: [PATCH 3/5] [CIR] Drop `cir.offload.container` and represent offload
modules through a unit market on MLIR modules.
---
.../include/clang/CIR/Dialect/IR/CIRAttrs.td | 42 +++++
.../include/clang/CIR/Dialect/IR/CIRDialect.h | 13 ++
.../clang/CIR/Dialect/IR/CIRDialect.td | 3 +
clang/include/clang/CIR/Dialect/IR/CIROps.td | 59 -------
clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 166 +++++++++++-------
.../test/CIR/IR/invalid-offload-container.cir | 70 +++++---
clang/test/CIR/IR/offload-container.cir | 38 ++--
7 files changed, 229 insertions(+), 162 deletions(-)
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index 838d8975177fd..691b0c311389d 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -115,6 +115,48 @@ def CIR_SourceLanguageAttr : CIR_EnumAttr<CIR_SourceLanguage, "lang"> {
}];
}
+//===----------------------------------------------------------------------===//
+// OffloadKindAttr
+//===----------------------------------------------------------------------===//
+
+def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
+ I32EnumAttrCase<"Host", 0, "host">,
+ I32EnumAttrCase<"Device", 1, "device">
+]> {
+ let genSpecializedAttr = 0;
+}
+
+def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
+ let summary = "Offload kind of a module in an offload container";
+ let description = [{
+ Marks a module as either the host module or one of the device modules of
+ an offload container, keeping a host CIR module and its associated device
+ CIR modules in one IR unit while offload merge/split pipeline passes need
+ visibility into both sides.
+
+ A module carrying the `cir.offload.container` unit attribute is such a
+ container. Its body holds only nested modules: the first one is the host
+ module and must carry `cir.offload.kind = #cir.offload_kind<host>`, and
+ all remaining ones are device modules and must carry
+ `cir.offload.kind = #cir.offload_kind<device>`. There must be at least one
+ device module. Keeping the host module first gives later passes a simple
+ convention for finding the host side while iterating the remaining device
+ modules.
+
+ Example:
+ ```mlir
+ module attributes {cir.offload.container} {
+ module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {}
+ module @device attributes {cir.offload.kind = #cir.offload_kind<device>} {}
+ }
+ ```
+
+ The attribute names `cir.offload.container` and `cir.offload.kind` are
+ defined by the `getOffloadContainerAttrName` and `getOffloadKindAttrName`
+ methods in the CIRDialect class.
+ }];
+}
+
//===----------------------------------------------------------------------===//
// ArgPassingKind + RecordLayoutAttr
//===----------------------------------------------------------------------===//
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
index 2f1ef5b6cb9e0..c61bd414036cb 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
@@ -88,6 +88,19 @@ RecordLayoutAttr getRecordLayout(mlir::ModuleOp mod, mlir::StringAttr name);
/// Same lookup as getRecordLayout, but returns a null attribute instead of
/// asserting when the record has no layout entry.
RecordLayoutAttr tryGetRecordLayout(mlir::ModuleOp mod, mlir::StringAttr name);
+RecordLayoutAttr getRecordLayout(mlir::ModuleOp module, mlir::StringAttr name);
+
+/// Returns whether the module is an offload container, i.e. whether it carries
+/// the cir.offload.container unit attribute. See CIR_OffloadKindAttr for the
+/// structure such a module is required to have.
+bool isOffloadContainer(mlir::ModuleOp module);
+
+/// Returns the host module of an offload container.
+mlir::ModuleOp getOffloadHostModule(mlir::ModuleOp container);
+
+/// Returns the device modules of an offload container, in container order.
+llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+getOffloadDeviceModules(mlir::ModuleOp container);
} // namespace cir
// TableGen'erated files for MLIR dialects require that a macro be defined when
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
index 00c8cfe008408..7c5c2d20bbaf4 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
@@ -26,6 +26,8 @@ def CIR_Dialect : Dialect {
let useDefaultAttributePrinterParser = 1;
+ let hasOperationAttrVerify = 1;
+
// Enable constant materialization for the CIR dialect. This generates a
// declaration for the cir::CIRDialect::materializeConstant function. This
// hook is necessary for canonicalization to properly handle attributes
@@ -53,6 +55,7 @@ def CIR_Dialect : Dialect {
static llvm::StringRef getModuleLevelAsmAttrName() { return "cir.module_asm"; }
static llvm::StringRef getGlobalCtorsAttrName() { return "cir.global_ctors"; }
static llvm::StringRef getGlobalDtorsAttrName() { return "cir.global_dtors"; }
+ static llvm::StringRef getOffloadContainerAttrName() { return "cir.offload.container"; }
static llvm::StringRef getOffloadKindAttrName() { return "cir.offload.kind"; }
static llvm::StringRef getOperandSegmentSizesAttrName() { return "operandSegmentSizes"; }
static llvm::StringRef getNoCallerSavedRegsAttrName() { return "no_caller_saved_registers"; }
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 158b97adfbe4e..6d149e6b1ccaa 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -6141,65 +6141,6 @@ def CIR_VecSplatOp : CIR_Op<"vec.splat", [
}];
}
-//===----------------------------------------------------------------------===//
-// OffloadKind
-//===----------------------------------------------------------------------===//
-
-def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
- I32EnumAttrCase<"Host", 0, "host">,
- I32EnumAttrCase<"Device", 1, "device">
-]> {
- let genSpecializedAttr = 0;
-}
-
-def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
- let summary = "Offload kind (host or device)";
-}
-
-//===----------------------------------------------------------------------===//
-// OffloadContainerOp
-//===----------------------------------------------------------------------===//
-
-def CIR_OffloadContainerOp : CIR_Op<"offload.container", [
- NoRegionArguments, NoTerminator, SingleBlock, SymbolTable]> {
- let summary = "Container for host and device CIR modules";
- let description = [{
- `cir.offload.container` groups one host CIR module with one or more device
- CIR modules for offload-aware analysis and transformation.
-
- The body holds nested `builtin.module` operations. The first nested
- module is the host module and must carry
- `cir.offload.kind = #cir.offload_kind<host>`. All remaining nested
- modules are device modules and must carry
- `cir.offload.kind = #cir.offload_kind<device>`. There must be at least
- one device module.
-
- Example:
-
- ```mlir
- cir.offload.container {
- builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
- }
- builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
- }
- }
- ```
- }];
-
- let regions = (region SizedRegion<1>:$body);
-
- let assemblyFormat = "$body attr-dict";
-
- let hasVerifier = 1;
- let hasLLVMLowering = false;
-
- let extraClassDeclaration = [{
- mlir::ModuleOp getHostModule();
- llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
- getDeviceModules();
- }];
-}
-
//===----------------------------------------------------------------------===//
// BaseClassAddrOp
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 5415e9285801d..45e3f011e6269 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -111,6 +111,112 @@ Operation *cir::CIRDialect::materializeConstant(mlir::OpBuilder &builder,
mlir::cast<mlir::TypedAttr>(value));
}
+//===----------------------------------------------------------------------===//
+// Offload container helpers
+//===----------------------------------------------------------------------===//
+
+bool cir::isOffloadContainer(mlir::ModuleOp module) {
+ return module->hasAttr(cir::CIRDialect::getOffloadContainerAttrName());
+}
+
+mlir::ModuleOp cir::getOffloadHostModule(mlir::ModuleOp container) {
+ assert(isOffloadContainer(container) && "expected an offload container");
+ return mlir::cast<mlir::ModuleOp>(container.getBody()->front());
+}
+
+llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+cir::getOffloadDeviceModules(mlir::ModuleOp container) {
+ assert(isOffloadContainer(container) && "expected an offload container");
+ mlir::Block &body = *container.getBody();
+ auto begin = body.op_begin<mlir::ModuleOp>();
+ auto end = body.op_end<mlir::ModuleOp>();
+ if (begin != end)
+ ++begin;
+ return {begin, end};
+}
+
+//===----------------------------------------------------------------------===//
+// Dialect attribute verification
+//===----------------------------------------------------------------------===//
+
+static LogicalResult verifyOffloadKind(mlir::ModuleOp module,
+ cir::OffloadKind expected) {
+ auto attr = module->getAttrOfType<cir::OffloadKindAttr>(
+ cir::CIRDialect::getOffloadKindAttrName());
+ if (!attr)
+ return module.emitOpError()
+ << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+ << "' offload kind attribute";
+ if (attr.getValue() != expected)
+ return module.emitOpError()
+ << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+ << "' value '" << cir::stringifyOffloadKind(expected) << "'";
+ return success();
+}
+
+// A module marked with `cir.offload.container` holds the host module followed
+// by one or more device modules, each tagged with `cir.offload.kind`. Keeping
+// the host module first gives later offload passes a simple convention for
+// finding the host side while iterating the remaining device modules.
+static LogicalResult verifyOffloadContainer(mlir::Operation *op) {
+ auto container = mlir::dyn_cast<mlir::ModuleOp>(op);
+ if (!container)
+ return op->emitError() << "expects '"
+ << cir::CIRDialect::getOffloadContainerAttrName()
+ << "' attribute to be attached to '"
+ << mlir::ModuleOp::getOperationName() << "'";
+
+ mlir::Block &body = *container.getBody();
+ if (body.empty())
+ return container.emitOpError()
+ << "expects host module as the first nested op";
+
+ auto host = mlir::dyn_cast<mlir::ModuleOp>(body.front());
+ if (!host)
+ return container.emitOpError()
+ << "expects host module as the first nested op";
+ if (failed(verifyOffloadKind(host, cir::OffloadKind::Host)))
+ return failure();
+
+ unsigned numDevices = 0;
+ auto it = body.begin();
+ for (++it; it != body.end(); ++it) {
+ auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
+ if (!module)
+ return container.emitOpError()
+ << "expects only nested builtin.module ops";
+ if (failed(verifyOffloadKind(module, cir::OffloadKind::Device)))
+ return failure();
+ ++numDevices;
+ }
+
+ if (numDevices == 0)
+ return container.emitOpError() << "expects at least one device module";
+ return success();
+}
+
+LogicalResult
+cir::CIRDialect::verifyOperationAttribute(mlir::Operation *op,
+ mlir::NamedAttribute attr) {
+ if (attr.getName() == getOffloadContainerAttrName()) {
+ if (!mlir::isa<mlir::UnitAttr>(attr.getValue()))
+ return op->emitError() << "expects '" << getOffloadContainerAttrName()
+ << "' to be a unit attribute";
+ return verifyOffloadContainer(op);
+ }
+
+ // The container verifier owns the structural contract between a container
+ // and the modules it holds. All this can add is that the kind attribute
+ // never lands on something that is not a module.
+ if (attr.getName() == getOffloadKindAttrName() &&
+ !mlir::isa<mlir::ModuleOp>(op))
+ return op->emitError() << "expects '" << getOffloadKindAttrName()
+ << "' attribute to be attached to '"
+ << mlir::ModuleOp::getOperationName() << "'";
+
+ return success();
+}
+
//===----------------------------------------------------------------------===//
// Helpers
//===----------------------------------------------------------------------===//
@@ -2375,66 +2481,6 @@ LogicalResult cir::VTTAddrPointOp::verify() {
return success();
}
-//===----------------------------------------------------------------------===//
-// OffloadContainerOp
-//===----------------------------------------------------------------------===//
-
-mlir::ModuleOp cir::OffloadContainerOp::getHostModule() {
- return mlir::cast<mlir::ModuleOp>(getBody().front().front());
-}
-
-llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
-cir::OffloadContainerOp::getDeviceModules() {
- mlir::Block &body = getBody().front();
- auto begin = body.op_begin<mlir::ModuleOp>();
- auto end = body.op_end<mlir::ModuleOp>();
- if (begin != end)
- ++begin;
- return {begin, end};
-}
-
-static LogicalResult checkOffloadKind(mlir::ModuleOp module,
- cir::OffloadKind expected) {
- auto attr = module->getAttrOfType<cir::OffloadKindAttr>(
- cir::CIRDialect::getOffloadKindAttrName());
- if (!attr)
- return module.emitOpError()
- << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
- << "' offload kind attribute";
- if (attr.getValue() != expected)
- return module.emitOpError()
- << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
- << "' value '" << cir::stringifyOffloadKind(expected) << "'";
- return success();
-}
-
-LogicalResult cir::OffloadContainerOp::verify() {
- mlir::Block &body = getBody().front();
- if (body.empty())
- return emitOpError() << "expects host module as the first nested op";
-
- auto host = mlir::dyn_cast<mlir::ModuleOp>(body.front());
- if (!host)
- return emitOpError() << "expects host module as the first nested op";
- if (failed(checkOffloadKind(host, cir::OffloadKind::Host)))
- return failure();
-
- unsigned numDevices = 0;
- auto it = body.begin();
- for (++it; it != body.end(); ++it) {
- auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
- if (!module)
- return emitOpError() << "expects only nested builtin.module ops";
- if (failed(checkOffloadKind(module, cir::OffloadKind::Device)))
- return failure();
- ++numDevices;
- }
-
- if (numDevices == 0)
- return emitOpError() << "expects at least one device module";
- return success();
-}
-
//===----------------------------------------------------------------------===//
// FuncOp
//===----------------------------------------------------------------------===//
diff --git a/clang/test/CIR/IR/invalid-offload-container.cir b/clang/test/CIR/IR/invalid-offload-container.cir
index 219442f39674a..92d4213765489 100644
--- a/clang/test/CIR/IR/invalid-offload-container.cir
+++ b/clang/test/CIR/IR/invalid-offload-container.cir
@@ -1,54 +1,80 @@
// RUN: cir-opt %s -verify-diagnostics -split-input-file
-module {
- cir.offload.container {
- builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} { // expected-error {{expects 'cir.offload.kind' value 'host'}}
- }
+module attributes {cir.offload.container} {
+ module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} { // expected-error {{expects 'cir.offload.kind' value 'host'}}
}
}
// -----
-module {
- cir.offload.container {
- builtin.module @host_0 attributes {cir.offload.kind = #cir.offload_kind<host>} {
- }
- builtin.module @host_1 attributes {cir.offload.kind = #cir.offload_kind<host>} { // expected-error {{expects 'cir.offload.kind' value 'device'}}
- }
+module attributes {cir.offload.container} {
+ module @host_0 attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ module @host_1 attributes {cir.offload.kind = #cir.offload_kind<host>} { // expected-error {{expects 'cir.offload.kind' value 'device'}}
}
}
// -----
-module {
- cir.offload.container {
- builtin.module @host { // expected-error {{expects 'cir.offload.kind' offload kind attribute}}
- }
+module attributes {cir.offload.container} {
+ module @host { // expected-error {{expects 'cir.offload.kind' offload kind attribute}}
+ }
+}
+
+// -----
+
+// expected-error at +1 {{expects only nested builtin.module ops}}
+module attributes {cir.offload.container} {
+ module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ cir.func @f() {
+ cir.return
+ }
+}
+
+// -----
+
+// expected-error at +1 {{expects at least one device module}}
+module attributes {cir.offload.container} {
+ module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
}
}
// -----
module {
- cir.offload.container { // expected-error {{expects only nested builtin.module ops}}
- builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
- }
- cir.const #cir.int<0> : !cir.int<s, 32>
+ // The op below keeps the CIR dialect loaded, which is what makes the
+ // dialect attribute verifier run on the empty container module.
+ cir.func @f() {
+ cir.return
+ }
+ // expected-error at +1 {{expects host module as the first nested op}}
+ module @container attributes {cir.offload.container} {
}
}
// -----
module {
- cir.offload.container { // expected-error {{expects at least one device module}}
- builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
- }
+ cir.func @f() {
+ cir.return {cir.offload.container} // expected-error {{expects 'cir.offload.container' attribute to be attached to 'builtin.module'}}
}
}
// -----
module {
- cir.offload.container { // expected-error {{expects host module as the first nested op}}
+ cir.func @f() {
+ cir.return {cir.offload.kind = #cir.offload_kind<host>} // expected-error {{expects 'cir.offload.kind' attribute to be attached to 'builtin.module'}}
+ }
+}
+
+// -----
+
+// expected-error at +1 {{expects 'cir.offload.container' to be a unit attribute}}
+module attributes {cir.offload.container = 0 : i32} {
+ module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
}
}
diff --git a/clang/test/CIR/IR/offload-container.cir b/clang/test/CIR/IR/offload-container.cir
index b7b53aa973ce5..f1e00bc63fed7 100644
--- a/clang/test/CIR/IR/offload-container.cir
+++ b/clang/test/CIR/IR/offload-container.cir
@@ -1,32 +1,28 @@
// RUN: cir-opt %s -split-input-file --verify-roundtrip | FileCheck %s
-module {
- cir.offload.container {
- builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
- }
- builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
- }
+module attributes {cir.offload.container} {
+ module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
}
}
-// CHECK: cir.offload.container {
-// CHECK: builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
-// CHECK: builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+// CHECK: module attributes {cir.offload.container} {
+// CHECK: module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+// CHECK: module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
// -----
-module {
- cir.offload.container {
- builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
- }
- builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
- }
- builtin.module @device_1 attributes {cir.offload.kind = #cir.offload_kind<device>} {
- }
+module attributes {cir.offload.container} {
+ module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+ }
+ module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+ }
+ module @device_1 attributes {cir.offload.kind = #cir.offload_kind<device>} {
}
}
-// CHECK: cir.offload.container {
-// CHECK: builtin.module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
-// CHECK: builtin.module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
-// CHECK: builtin.module @device_1 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+// CHECK: module attributes {cir.offload.container} {
+// CHECK: module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+// CHECK: module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+// CHECK: module @device_1 attributes {cir.offload.kind = #cir.offload_kind<device>} {
>From 1b10091e99962aca0245127138d9133e48382a17 Mon Sep 17 00:00:00 2001
From: David Rivera <davidriverg at gmail.com>
Date: Wed, 2 Sep 2026 01:32:56 -0400
Subject: [PATCH 4/5] address various coments
---
.../include/clang/CIR/Dialect/IR/CIRAttrs.td | 32 +++++++++--------
.../include/clang/CIR/Dialect/IR/CIRDialect.h | 13 -------
clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 35 +++----------------
3 files changed, 23 insertions(+), 57 deletions(-)
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index 691b0c311389d..cadb8d0fa1f84 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -129,28 +129,32 @@ def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
let summary = "Offload kind of a module in an offload container";
let description = [{
- Marks a module as either the host module or one of the device modules of
- an offload container, keeping a host CIR module and its associated device
- CIR modules in one IR unit while offload merge/split pipeline passes need
- visibility into both sides.
-
- A module carrying the `cir.offload.container` unit attribute is such a
- container. Its body holds only nested modules: the first one is the host
- module and must carry `cir.offload.kind = #cir.offload_kind<host>`, and
- all remaining ones are device modules and must carry
- `cir.offload.kind = #cir.offload_kind<device>`. There must be at least one
- device module. Keeping the host module first gives later passes a simple
- convention for finding the host side while iterating the remaining device
- modules.
+ Marks a module nested inside an offload container - identified by the
+ `cir.offload.container` unit attribute - as either the host module (carrying
+ `cir.offload.kind = #cir.offload_kind<host>`) or one of the device modules
+ (carrying `cir.offload.kind = #cir.offload_kind<device>`) of the offload
+ container. This creates a tiered module layout, where a singular offload
+ container contains a single host module and one or more device modules,
+ granting the offload merge/split pipeline passes the cross-boundary view.
Example:
```mlir
module attributes {cir.offload.container} {
module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {}
- module @device attributes {cir.offload.kind = #cir.offload_kind<device>} {}
+ module @device1 attributes {cir.offload.kind = #cir.offload_kind<device>} {}
+ module @device2 attributes {cir.offload.kind = #cir.offload_kind<device>} {}
}
```
+ Note: In order to simplify offload passes, the offload container module
+ requires that the first module inside it is the host module and the rest
+ of the modules are device modules.
+
+ The nested modules are plain `builtin.module` ops rather than a dedicated
+ container op, so that the existing module infrastructure applies to them
+ unchanged; the layout above is enforced by the CIR dialect attribute
+ verifier, following the `gpu.container_module` precedent.
+
The attribute names `cir.offload.container` and `cir.offload.kind` are
defined by the `getOffloadContainerAttrName` and `getOffloadKindAttrName`
methods in the CIRDialect class.
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
index c61bd414036cb..2f1ef5b6cb9e0 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
@@ -88,19 +88,6 @@ RecordLayoutAttr getRecordLayout(mlir::ModuleOp mod, mlir::StringAttr name);
/// Same lookup as getRecordLayout, but returns a null attribute instead of
/// asserting when the record has no layout entry.
RecordLayoutAttr tryGetRecordLayout(mlir::ModuleOp mod, mlir::StringAttr name);
-RecordLayoutAttr getRecordLayout(mlir::ModuleOp module, mlir::StringAttr name);
-
-/// Returns whether the module is an offload container, i.e. whether it carries
-/// the cir.offload.container unit attribute. See CIR_OffloadKindAttr for the
-/// structure such a module is required to have.
-bool isOffloadContainer(mlir::ModuleOp module);
-
-/// Returns the host module of an offload container.
-mlir::ModuleOp getOffloadHostModule(mlir::ModuleOp container);
-
-/// Returns the device modules of an offload container, in container order.
-llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
-getOffloadDeviceModules(mlir::ModuleOp container);
} // namespace cir
// TableGen'erated files for MLIR dialects require that a macro be defined when
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 45e3f011e6269..16ecedd10690b 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -111,30 +111,6 @@ Operation *cir::CIRDialect::materializeConstant(mlir::OpBuilder &builder,
mlir::cast<mlir::TypedAttr>(value));
}
-//===----------------------------------------------------------------------===//
-// Offload container helpers
-//===----------------------------------------------------------------------===//
-
-bool cir::isOffloadContainer(mlir::ModuleOp module) {
- return module->hasAttr(cir::CIRDialect::getOffloadContainerAttrName());
-}
-
-mlir::ModuleOp cir::getOffloadHostModule(mlir::ModuleOp container) {
- assert(isOffloadContainer(container) && "expected an offload container");
- return mlir::cast<mlir::ModuleOp>(container.getBody()->front());
-}
-
-llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
-cir::getOffloadDeviceModules(mlir::ModuleOp container) {
- assert(isOffloadContainer(container) && "expected an offload container");
- mlir::Block &body = *container.getBody();
- auto begin = body.op_begin<mlir::ModuleOp>();
- auto end = body.op_end<mlir::ModuleOp>();
- if (begin != end)
- ++begin;
- return {begin, end};
-}
-
//===----------------------------------------------------------------------===//
// Dialect attribute verification
//===----------------------------------------------------------------------===//
@@ -178,20 +154,19 @@ static LogicalResult verifyOffloadContainer(mlir::Operation *op) {
if (failed(verifyOffloadKind(host, cir::OffloadKind::Host)))
return failure();
- unsigned numDevices = 0;
auto it = body.begin();
- for (++it; it != body.end(); ++it) {
+ ++it;
+ if (it == body.end())
+ return container.emitOpError() << "expects at least one device module";
+
+ for (; it != body.end(); ++it) {
auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
if (!module)
return container.emitOpError()
<< "expects only nested builtin.module ops";
if (failed(verifyOffloadKind(module, cir::OffloadKind::Device)))
return failure();
- ++numDevices;
}
-
- if (numDevices == 0)
- return container.emitOpError() << "expects at least one device module";
return success();
}
>From e2cf561b191db9e4fead2cff042dfeaecbd0dc5d Mon Sep 17 00:00:00 2001
From: David Rivera <davidriverg at gmail.com>
Date: Mon, 7 Sep 2026 16:55:27 -0400
Subject: [PATCH 5/5] [CIR] Migrate CIR_OffloadKind off the legacy EnumAttrInfo
hierarchy
CIR_I32EnumAttr and its genSpecializedAttr flag were removed by the
upstream enum migration (#220889); CIR_OffloadKind now derives from
CIR_I32Enum like the other CIR enums.
---
clang/include/clang/CIR/Dialect/IR/CIRAttrs.td | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index cadb8d0fa1f84..ac6f2718d2fcd 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -119,12 +119,10 @@ def CIR_SourceLanguageAttr : CIR_EnumAttr<CIR_SourceLanguage, "lang"> {
// OffloadKindAttr
//===----------------------------------------------------------------------===//
-def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
+def CIR_OffloadKind : CIR_I32Enum<"OffloadKind", "offload kind", [
I32EnumAttrCase<"Host", 0, "host">,
I32EnumAttrCase<"Device", 1, "device">
-]> {
- let genSpecializedAttr = 0;
-}
+]>;
def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
let summary = "Offload kind of a module in an offload container";
More information about the cfe-commits
mailing list