← Back to list

Emitting OpenMP Code from OMP Dialect and Lowering to LLVM IR

Introduction

Robert K Samuel · 2025-03-11 05:59 · 4 claps · 2.2 min read
#compilers #compiler-design #compiler-optimization #llvm #openmp
Open on Medium ↗

Emitting OpenMP Code from OMP Dialect and Lowering to LLVM IR

Photo by Orfeas Green on Unsplash

Photo by Orfeas Green on Unsplash

Introduction

In this blog, we’ll explore how to emit OpenMP (omp) code using the MLIR OpenMP dialect, lower it to LLVM, and generate an executable binary.

We’ll go through the key steps:

  1. Constructing an MLIR module with OpenMP parallel execution.
  2. Lowering the MLIR code to LLVM IR.
  3. Translating LLVM IR to a final executable using Clang.

Setting Up the MLIR Context

The first step is to create an MLIR context and load the necessary dialects. In our case, we need the OpenMP (omp) and LLVM dialects.

MLIRContext context;

// Load the required dialects
context.getOrLoadDialect<mlir::omp::OpenMPDialect>();
context.getOrLoadDialect<mlir::LLVM::LLVMDialect>();

mlir::OpBuilder builder(&context);
mlir::ModuleOp module = builder.create<mlir::ModuleOp>(builder.getUnknownLoc());

This initializes the MLIR context and prepares it for generating operations.

Defining a Global String and Print Function

To print a message in OpenMP threads, we need:

  1. A global string ("Hello World\n").
  2. The printf function.

Defining a Global String

auto i8Type = builder.getI8Type();
auto i8PtrType = mlir::LLVM::LLVMPointerType::get(&context);
const char *str = "Hello World\n\0";
size_t len = strlen(str);
auto arrayType = mlir::LLVM::LLVMArrayType::get(i8Type, len);

auto globalStr = builder.create<mlir::LLVM::GlobalOp>(
    builder.getUnknownLoc(), arrayType, true, 
    mlir::LLVM::Linkage::Internal, ".str", 
    builder.getStringAttr(str));
module.push_back(globalStr);

Declaring printf

auto printfType = mlir::LLVM::LLVMFunctionType::get(i8PtrType, {i8PtrType}, true);
auto printfFunc = builder.create<mlir::LLVM::LLVMFuncOp>(
    builder.getUnknownLoc(), "printf", printfType);
module.push_back(printfFunc);

Creating the main Function

Now, we define the main function, retrieve the address of our global string, and prepare it for printing.

auto int32Ty = builder.getI32Type();
auto funcType = LLVM::LLVMFunctionType::get(int32Ty, {int32Ty});

auto funcOp = builder.create<mlir::LLVM::LLVMFuncOp>(
    builder.getUnknownLoc(), "main", funcType);
module.push_back(funcOp);

auto block = funcOp.addEntryBlock(builder);
builder.setInsertionPointToStart(block);

// Get pointer to string
auto zero = builder.create<mlir::LLVM::ConstantOp>(
    builder.getUnknownLoc(), int32Ty, builder.getI32IntegerAttr(0));
auto strPtr = builder.create<mlir::LLVM::AddressOfOp>(
    builder.getUnknownLoc(), globalStr);

auto gep = builder.create<mlir::LLVM::GEPOp>(
    builder.getUnknownLoc(), i8PtrType, arrayType, strPtr, 
    mlir::ValueRange{zero, zero});

Here, we create the main function, allocate a block, and generate a pointer (gep) to access our string.

Emitting OpenMP Parallel Region

We now insert an OpenMP parallel block that calls printf:

mlir::Location dummyLoc = mlir::FileLineColLoc::get(
    builder.getStringAttr("dummy.mlir"), 0, 0);
auto parallelOp = builder.create<mlir::omp::ParallelOp>(dummyLoc);
auto &parallelRegion = parallelOp.getRegion();
auto *parallelBlock = builder.createBlock(&parallelRegion);
builder.setInsertionPointToStart(parallelBlock);

// Call printf inside parallel region
builder.create<mlir::LLVM::CallOp>(
    builder.getUnknownLoc(), printfFunc, mlir::ValueRange{gep});
builder.create<mlir::omp::TerminatorOp>(builder.getUnknownLoc());
builder.setInsertionPointAfter(parallelOp);

This OpenMP parallel region ensures that multiple threads execute printf.

Finalizing the Function

We conclude by returning 33 as the exit status.

auto constOp = builder.create<mlir::LLVM::ConstantOp>(
    builder.getUnknownLoc(), int32Ty, builder.getI32IntegerAttr(33));
auto retOp = builder.create<mlir::LLVM::ReturnOp>(
    builder.getUnknownLoc(), constOp->getResult(0));

Before proceeding, we verify the MLIR module:

if (failed(verify(module))) {
    llvm::errs() << "Error: Verification Failed\n";
    return 0;
}

Lowering to LLVM IR

To generate LLVM IR, we apply the OpenMP-to-LLVM conversion pass:

PassManager pm(&context);
pm.addPass(createConvertOpenMPToLLVMPass());

if (failed(pm.run(module))) {
    llvm::errs() << "Failed to run passes\n";
    return 1;
}
module->dump();

Translating MLIR to LLVM IR

mlir-translate --mlir-to-llvmir output.mlir -o output.ll

Compiling and Running the Executable

clang output.ll -o a.out -fopenmp
./a.out

Expected output:

Hello World
Hello World
Hello World
Hello World
Hello World
Hello World
Hello World
Hello World

The number of lines printed depends on the system’s OpenMP thread count.

Conclusion

We successfully:

  1. Emitted OpenMP code in MLIR.
  2. Lowered it to LLVM IR.
  3. Compiled and executed the OpenMP-enabled program.

MLIR’s OpenMP dialect makes it easy to integrate parallel execution while maintaining flexibility for optimizations and transformations.

The complete code is available in our GitHub repository: Blog CodeBase Repo.


메타데이터
post_id
dd725d529005
slug
emitting-openmp-code-from-mlir-and-lowering-to-llvm-ir-dd725d529005
url
https://medium.com/@60b36t/emitting-openmp-code-from-mlir-and-lowering-to-llvm-ir-dd725d529005
canonical_url
https://medium.com/@60b36t/emitting-openmp-code-from-mlir-and-lowering-to-llvm-ir-dd725d529005
author_url
https://medium.com/@60b36t
status
ok
fetched_at
2026-06-20 20:29:01