https://github.com/RiverDave created https://github.com/llvm/llvm-project/pull/206576
Introduce `cir.offload.container`, a CIR dialect operation used to keep a host CIR module and its associated device CIR modules in one IR unit while later offload merge/split pipeline passes need visibility into both sides. The container owns a single-block region containing nested `builtin.module` operations. Each nested module is tagged with `cir.offload.kind`, represented by the new `#cir.offload_kind<host>` / `#cir.offload_kind<device>` enum attribute. The verifier enforces the structural contract expected by the follow-up pipeline work: - the first nested module is the host module - all following nested modules are device modules - the body contains only nested `builtin.module` operations - at least one device module is present Keeping the host module first gives later passes a simple convention for finding the host side while iterating the remaining device modules. This patch only introduces the IR representation and verifier tests. The passes that create, consume, or split this container are left to follow-up patches. This is part of the Host-Device CIR Combine Pipeline work discussed on Discourse: https://discourse.llvm.org/t/gsoc-2026-host-device-cir-combine-pipeline-project-idea-discussion/89623 >From 9d0f66f6fe743d75f3cae14b5e7412e646b6116a Mon Sep 17 00:00:00 2001 From: David Rivera <[email protected]> Date: Mon, 29 Jun 2026 15:46:11 -0400 Subject: [PATCH] [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 c20af04f97a1a..7d189361eee48 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td +++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td @@ -50,6 +50,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 7d1c48b994b27..0c18aac7f4eb4 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIROps.td +++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td @@ -5672,6 +5672,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 67cc5e09f26d0..96501d5808168 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" @@ -2334,6 +2335,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>} { _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
