{"schemaVersion":1,"id":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects","title":"LLVM MLIR: Building Custom Compiler Dialects for Domain-Specific Optimization","description":"An examination of MLIR's multi-level intermediate representation architecture, custom dialect construction, and the convergence of ML compilers, hardware accelerators, and DSLs on the MLIR framework.","language":"en","datePublished":"2026-01-27","dateModified":null,"contentFormat":"text/markdown","content":"Compilers\n\n# LLVM MLIR: Building Custom Compiler Dialects for Domain-Specific Optimization\n\n2026-01-2711 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/llvm-mlir-compiler-infrastructure-custom-dialects>)\n\n## The Problem: Why LLVM IR Wasn't Enough\n\n[LLVM](<https://llvm.org/>) is effective for what it does: take moderately low-level IR and turn it into optimized machine code for a dozen architectures. But it has a fundamental limitation, there's only one level of IR, and it sits at roughly the abstraction level of C.\n\nIf you're building a compiler for a high-level language (say, a tensor processing DSL), you have to lower your high-level concepts to LLVM IR early. Once you do, information is lost. The LLVM optimizer sees loops and pointer arithmetic, not matrix multiplications and convolutions. It can't fuse two matrix operations that it doesn't know are matrix operations.\n\nThis is the \"abstraction gap\" that plagued ML compilers for years:\n\n```\nHigh-level DSL: matmul(A, B) + transpose(C)\n │\n ┌───────────────┤ (information loss here)\n ▼ │\nLLVM IR: nested loops, gep instructions, loads/stores\n → LLVM can optimize the loops but can't see the linear algebra\n```\n\nEvery ML framework solved this differently: TensorFlow had XLA, PyTorch had TorchScript then Torch-MLIR, TVM had its own IR, Halide had its own IR. Each one reinvented type systems, SSA construction, pass infrastructure, and canonicalization. Massive duplication.\n\n## MLIR's Core Insight: Dialects All the Way Down\n\nMLIR's key insight is that you don't need one IR, you need a framework for building many IRs that can coexist and interoperate. That framework is the **dialect** system.\n\nA dialect is a namespace containing:\n\n- **Operations** (the \"instructions\" of your IR)\n- **Types** (the type system for your domain)\n- **Attributes** (compile-time constants and metadata)\n- **Interfaces** (traits that operations can implement for generic transformations)\n\nThe magic is that operations from different dialects can exist in the same IR, and you can progressively lower from high-level dialects to low-level ones:\n\n```mlir\n// High-level: linalg dialect (linear algebra operations)\n%result = linalg.matmul ins(%A, %B : tensor<64x128xf32>, tensor<128x256xf32>)\n outs(%C : tensor<64x256xf32>) -> tensor<64x256xf32>\n\n// Mid-level: affine dialect (polyhedral loop nests)\naffine.for %i = 0 to 64 {\n affine.for %j = 0 to 256 {\n affine.for %k = 0 to 128 {\n %a = affine.load %A[%i, %k] : memref<64x128xf32>\n %b = affine.load %B[%k, %j] : memref<128x256xf32>\n %c = affine.load %C[%i, %j] : memref<64x256xf32>\n %prod = arith.mulf %a, %b : f32\n %sum = arith.addf %c, %prod : f32\n affine.store %sum, %C[%i, %j] : memref<64x256xf32>\n }\n }\n}\n\n// Low-level: llvm dialect (LLVM IR operations)\n%ptr = llvm.getelementptr %base[%idx] : (!llvm.ptr, i64) -> !llvm.ptr\n%val = llvm.load %ptr : !llvm.ptr -> f32\n```\n\nAt each level, you can apply domain-specific optimizations that would be impossible at lower levels. The ` linalg ` level knows about tiling and fusion strategies for matrix operations. The ` affine ` level can apply polyhedral transformations. The ` llvm ` level does register allocation and instruction selection.\n\n## Building a Custom Dialect: A Toy Example\n\nThe following example builds a simple dialect for a hypothetical image processing DSL. It defines operations for loading images, applying filters, and compositing.\n\n### Step 1: Define the Dialect in TableGen\n\n```tablegen\n// ImageOps.td\ninclude \"mlir/IR/OpBase.td\"\ninclude \"mlir/IR/AttrTypeBase.td\"\ninclude \"mlir/Interfaces/SideEffectInterfaces.td\"\n\n// Define the dialect\ndef ImageDialect : Dialect {\n let name = \"image\";\n let cppNamespace = \"::image\";\n let summary = \"A dialect for image processing operations\";\n let description = [{\n The image dialect provides operations for loading, transforming,\n and compositing images with type-safe channel information.\n }];\n}\n\n// Define a custom type for images with channel info\ndef Image_ImageType : TypeDef<ImageDialect, \"Image\"> {\n let mnemonic = \"img\";\n let parameters = (ins\n \"int64_t\":$width,\n \"int64_t\":$height,\n \"int64_t\":$channels // 1=gray, 3=RGB, 4=RGBA\n );\n let assemblyFormat = \"`<` $width `,` $height `,` $channels `>`\";\n}\n\n// Load operation\ndef Image_LoadOp : Op<ImageDialect, \"load\", [Pure]> {\n let summary = \"Load an image from a path\";\n let arguments = (ins StrAttr:$path);\n let results = (outs Image_ImageType:$result);\n let assemblyFormat = \"$path attr-dict `:` type($result)\";\n}\n\n// Blur operation\ndef Image_BlurOp : Op<ImageDialect, \"blur\", [Pure]> {\n let summary = \"Apply Gaussian blur\";\n let arguments = (ins\n Image_ImageType:$input,\n F32Attr:$sigma\n );\n let results = (outs Image_ImageType:$result);\n let assemblyFormat = \"$input `,` $sigma attr-dict `:` type($input) `->` type($result)\";\n\n // Verify input and output have same dimensions\n let verifier = [{\n auto inType = getInput().getType().cast<ImageType>();\n auto outType = getResult().getType().cast<ImageType>();\n if (inType.getWidth() != outType.getWidth() ||\n inType.getHeight() != outType.getHeight())\n return emitOpError(\"input and output dimensions must match\");\n return success();\n }];\n}\n\n// Composite operation\ndef Image_CompositeOp : Op<ImageDialect, \"composite\", [Pure]> {\n let summary = \"Composite two images with blend mode\";\n let arguments = (ins\n Image_ImageType:$foreground,\n Image_ImageType:$background,\n StrAttr:$blend_mode // \"over\", \"multiply\", \"screen\"\n );\n let results = (outs Image_ImageType:$result);\n let assemblyFormat = [{\n $foreground `,` $background `,` $blend_mode attr-dict `:`\n type($foreground) `,` type($background) `->` type($result)\n }];\n}\n```\n\n### Step 2: Write a Domain-Specific Optimization Pass\n\nNow let's write a pass that fuses consecutive blur operations into a single blur with combined sigma:\n\n```cpp\n// FuseBlursPass.cpp\n#include \"mlir/Pass/Pass.h\"\n#include \"ImageOps.h\"\n#include <cmath>\n\nnamespace {\nstruct FuseBlursPass\n : public mlir::PassWrapper<FuseBlursPass,\n mlir::OperationPass<mlir::func::FuncOp>> {\n\n void runOnOperation() override {\n getOperation().walk([](image::BlurOp outerBlur) {\n // Check if the input to this blur is another blur\n auto innerBlur = outerBlur.getInput().getDefiningOp<image::BlurOp>();\n if (!innerBlur)\n return;\n\n // Check that the inner blur has no other users\n if (!innerBlur.getResult().hasOneUse())\n return;\n\n // Fuse: sigma_combined = sqrt(sigma1^2 + sigma2^2)\n float sigma1 = innerBlur.getSigma().convertToFloat();\n float sigma2 = outerBlur.getSigma().convertToFloat();\n float fusedSigma = std::sqrt(sigma1 * sigma1 + sigma2 * sigma2);\n\n // Replace the outer blur with a single blur on the inner's input\n mlir::OpBuilder builder(outerBlur);\n auto fusedBlur = builder.create<image::BlurOp>(\n outerBlur.getLoc(),\n outerBlur.getType(),\n innerBlur.getInput(),\n builder.getF32FloatAttr(fusedSigma));\n\n outerBlur.replaceAllUsesWith(fusedBlur.getResult());\n outerBlur.erase();\n if (innerBlur.getResult().use_empty())\n innerBlur.erase();\n });\n }\n\n mlir::StringRef getArgument() const final { return \"image-fuse-blurs\"; }\n mlir::StringRef getDescription() const final {\n return \"Fuse consecutive blur operations\";\n }\n};\n} // namespace\n\n// Register the pass\nstd::unique_ptr<mlir::Pass> createFuseBlursPass() {\n return std::make_unique<FuseBlursPass>();\n}\n```\n\n### Step 3: Lower to Standard Dialects\n\nEventually, we need to lower our image operations to something LLVM can compile. Here's a sketch of lowering ` image.blur ` to loop nests:\n\n```cpp\nstruct BlurOpLowering : public mlir::OpConversionPattern<image::BlurOp> {\n using OpConversionPattern::OpConversionPattern;\n\n mlir::LogicalResult matchAndRewrite(\n image::BlurOp op,\n OpAdaptor adaptor,\n mlir::ConversionPatternRewriter &rewriter) const override {\n\n auto loc = op.getLoc();\n auto imgType = op.getInput().getType().cast<image::ImageType>();\n int64_t width = imgType.getWidth();\n int64_t height = imgType.getHeight();\n int64_t channels = imgType.getChannels();\n float sigma = op.getSigma().convertToFloat();\n\n // Compute kernel size from sigma\n int kernelSize = static_cast<int>(std::ceil(sigma * 3.0f)) * 2 + 1;\n int halfKernel = kernelSize / 2;\n\n // Allocate output buffer\n auto memrefType = mlir::MemRefType::get(\n {height, width, channels}, rewriter.getF32Type());\n auto output = rewriter.create<mlir::memref::AllocOp>(loc, memrefType);\n\n // Generate nested loops for convolution\n // (simplified, real implementation would generate separable passes)\n auto yLoop = rewriter.create<mlir::affine::AffineForOp>(\n loc, 0, height, 1);\n rewriter.setInsertionPointToStart(yLoop.getBody());\n\n auto xLoop = rewriter.create<mlir::affine::AffineForOp>(\n loc, 0, width, 1);\n rewriter.setInsertionPointToStart(xLoop.getBody());\n\n // ... kernel computation loop body ...\n\n rewriter.replaceOp(op, output);\n return mlir::success();\n }\n};\n```\n\n## The IR In Action\n\nHere's what a complete pipeline looks like in our image dialect:\n\n```mlir\n// Input IR (high-level image operations)\nfunc.func @process_photo(%path: !image.img<1920, 1080, 3>) -> !image.img<1920, 1080, 3> {\n %photo = image.load \"/photos/input.jpg\" : !image.img<1920, 1080, 3>\n %blurred1 = image.blur %photo, 2.0 : !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n %blurred2 = image.blur %blurred1, 3.0 : !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n %overlay = image.load \"/photos/overlay.png\" : !image.img<1920, 1080, 4>\n %result = image.composite %overlay, %blurred2, \"over\" :\n !image.img<1920, 1080, 4>, !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n return %result : !image.img<1920, 1080, 3>\n}\n\n// After blur fusion pass (sigma = sqrt(4 + 9) = 3.606):\nfunc.func @process_photo(%path: !image.img<1920, 1080, 3>) -> !image.img<1920, 1080, 3> {\n %photo = image.load \"/photos/input.jpg\" : !image.img<1920, 1080, 3>\n %blurred = image.blur %photo, 3.606 : !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n %overlay = image.load \"/photos/overlay.png\" : !image.img<1920, 1080, 4>\n %result = image.composite %overlay, %blurred, \"over\" :\n !image.img<1920, 1080, 4>, !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n return %result : !image.img<1920, 1080, 3>\n}\n```\n\nThe fusion pass turned two blur operations into one, cutting the convolution work roughly in half. This optimization is trivial to express at the image dialect level, but would be nearly impossible to discover after lowering to loops.\n\n## Real-World MLIR: The ML Compiler Landscape\n\nThe biggest users of MLIR are ML compilers, and for good reason. ML workloads have a natural multi-level structure:\n\n```\nFramework level: torch.nn.Linear(256, 128)\n │\n ▼ TorchMLIR\nGraph level: linalg.matmul + linalg.add (bias)\n │\n ▼ Tiling + Fusion\nTile level: scf.for loops with vectorized inner bodies\n │\n ▼ Hardware lowering\nHardware level: gpu.launch_func / vector.contract / amx.tile_mulf\n │\n ▼ LLVM lowering\nMachine code: AVX-512 / CUDA PTX / AMX instructions\n```\n\nEach level has its own optimization opportunities:\n\n- **Graph level**: operator fusion (matmul + bias + relu = one kernel), layout optimization (NHWC vs NCHW)\n- **Tile level**: tile size selection, cache-aware scheduling, vectorization strategy\n- **Hardware level**: mapping to specific ISA extensions (AMX for Intel, Tensor Cores for NVIDIA)\n\n### IREE: Google's ML Runtime Built on MLIR\n\nIREE (Intermediate Representation Execution Environment) is probably the most complete MLIR-based ML compiler in production. It takes TensorFlow, PyTorch, or JAX models, lowers them through MLIR dialects, and generates optimized code for CPUs, GPUs, and accelerators.\n\nThe compilation pipeline:\n\n```\nInput: StableHLO (from JAX/TF) or Torch-MLIR (from PyTorch)\n → IREE Input dialect (normalize all inputs)\n → IREE Flow dialect (partition into dispatches, manage buffers)\n → IREE Stream dialect (async execution scheduling)\n → IREE HAL dialect (hardware abstraction)\n → Target-specific codegen (LLVM CPU, SPIR-V, CUDA, Metal)\n```\n\nEach arrow is a series of MLIR passes that lower and optimize. The whole pipeline is around 80 passes, and every intermediate state is printable, verifiable MLIR.\n\n### Mojo: A Language Built on MLIR\n\nModular's Mojo programming language is built directly on MLIR. This means Mojo's compiler can express high-level abstractions (Python-like syntax) and low-level hardware operations (SIMD intrinsics) in the same IR, and the optimization pipeline can see both simultaneously.\n\n```mojo\n# Mojo code that compiles through MLIR\nfn matmul_tiled[M: Int, N: Int, K: Int](\n A: SIMD[DType.float32, M*K],\n B: SIMD[DType.float32, K*N],\n) -> SIMD[DType.float32, M*N]:\n # This compiles to MLIR linalg ops, then gets tiled and vectorized\n # through the standard MLIR pipeline\n var C = SIMD[DType.float32, M*N](0)\n for i in range(M):\n for j in range(N):\n for k in range(K):\n C[i*N + j] += A[i*K + k] * B[k*N + j]\n return C\n```\n\nMojo's compiler sees the ` SIMD ` types and loop structure, matches it against tiling patterns, and generates code that uses hardware vector instructions directly. The MLIR infrastructure handles the type checking, SSA construction, and pass scheduling, Mojo's compiler team focuses on the domain-specific patterns.\n\n## Getting Started: The Practical Path\n\nFor those wanting to experiment with MLIR, here is a recommended path:\n\n### 1. Build MLIR from Source\n\n```bash\ngit clone https://github.com/llvm/llvm-project.git\ncd llvm-project\nmkdir build && cd build\n\ncmake -G Ninja ../llvm \\\n -DLLVM_ENABLE_PROJECTS=mlir \\\n -DLLVM_BUILD_EXAMPLES=ON \\\n -DLLVM_TARGETS_TO_BUILD=\"host\" \\\n -DCMAKE_BUILD_TYPE=Release \\\n -DLLVM_ENABLE_ASSERTIONS=ON\n\nninja -j$(nproc)\n```\n\n### 2. Play with mlir-opt\n\n```bash\n# Write a simple MLIR program\ncat > test.mlir << 'EOF'\nfunc.func @add(%arg0: f32, %arg1: f32) -> f32 {\n %0 = arith.addf %arg0, %arg1 : f32\n return %0 : f32\n}\nEOF\n\n# Run canonicalization\n./bin/mlir-opt --canonicalize test.mlir\n\n# Lower to LLVM dialect\n./bin/mlir-opt --convert-func-to-llvm --convert-arith-to-llvm test.mlir\n\n# See all available passes\n./bin/mlir-opt --help | grep \"Passes:\"\n```\n\n### 3. Study the Toy Tutorial\n\nThe MLIR repo includes a complete tutorial that builds a compiler for a toy language from scratch. It's in ` mlir/docs/Tutorials/Toy/ ` and covers everything from lexing to LLVM codegen through MLIR.\n\n### 4. Look at Real Dialects\n\nStudy the ` linalg ` and ` affine ` dialect implementations in the MLIR source tree. They're well-documented and demonstrate production patterns for op definitions, transformations, and lowering.\n\n## Why This Matters Beyond Compilers\n\nMLIR's dialect system is genuinely useful outside traditional compiler work:\n\n- **Hardware design**: CIRCT (Circuit IR Compilers and Tools) uses MLIR dialects to represent hardware description languages, replacing Verilog/VHDL toolchains.\n- **Quantum computing**: The ` quake ` dialect represents quantum circuits for optimization before mapping to quantum hardware.\n- **Database engines**: Query planners can use custom dialects to represent relational algebra and optimize execution plans.\n- **Numerical simulation**: The ` sparse_tensor ` dialect handles sparse matrix operations with automatic format selection.\n\nThe pattern is always the same: you have a domain with specific operations and optimization rules that are invisible at a lower abstraction level. MLIR gives you the infrastructure to express that domain without building everything from scratch.\n\nMLIR isn't just a better LLVM IR. It's a framework for building compilers at any abstraction level, with infrastructure that took decades to develop in LLVM, available out of the box. If you're building anything that transforms structured representations, whether that's ML models, hardware descriptions, or database queries, MLIR is worth your time.\n\n## Frequently asked questions\n\n### What is MLIR?\n\nMLIR (Multi-Level Intermediate Representation) is a compiler infrastructure within the LLVM project that provides a flexible, extensible framework for building domain-specific compilers using composable 'dialects', collections of types, operations, and transformation rules.\n\n### How is MLIR different from LLVM IR?\n\nLLVM IR is a single, fixed-level representation designed for general-purpose CPU code generation. MLIR supports multiple abstraction levels simultaneously, letting you represent high-level domain concepts (tensor operations, hardware intrinsics) alongside low-level code.\n\n### What are MLIR dialects?\n\nDialects are modular collections of operations, types, and attributes in MLIR. Each dialect defines its own vocabulary, for example, the 'linalg' dialect has operations for linear algebra, while the 'gpu' dialect has operations for GPU kernels. You can mix dialects freely in the same IR.\n\n### Who uses MLIR in production?\n\nGoogle (TensorFlow/XLA, IREE), AMD (ROCm compiler stack), Intel (oneAPI), Modular (Mojo language), and several ML hardware startups use MLIR as their compiler backbone.\n\n## Tips\n\n1. **Tip:** When defining custom MLIR ops, always use TableGen (` .td ` files) instead of hand-writing C++ op classes. TableGen auto-generates verifiers, builders, parsers, and printers. Writing ops by hand is a debugging nightmare.\n2. **Tip:** MLIR's ` -mlir-print-ir-after-all ` flag dumps the IR after every pass. Pipe it through ` diff ` to see exactly what each transformation does. This is the single most useful debugging technique for pass development.\n3. **Tip:** Start with the ` func ` and ` arith ` dialects when learning MLIR. They're simple enough to understand in an afternoon but demonstrate all the core concepts, regions, blocks, SSA values, and type systems.\n4. **Tip:** Use ` mlir-opt --canonicalize ` as your first optimization pass. Canonicalization patterns (constant folding, dead code elimination, identity removal) are registered per-op and run to fixpoint. It catches 80% of obvious redundancies.\n\n- llvm\n- mlir\n- compilers\n- dialects\n- optimization\n- machine-learning\n- dsl\n- intermediate-representation\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"LLVM MLIR: Building Custom Compiler Dialects for Domain-Specific Optimization","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#title"},{"level":2,"text":"The Problem: Why LLVM IR Wasn't Enough","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#the-problem-why-llvm-ir-wasnt-enough"},{"level":2,"text":"MLIR's Core Insight: Dialects All the Way Down","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#mlirs-core-insight-dialects-all-the-way-down"},{"level":2,"text":"Building a Custom Dialect: A Toy Example","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#building-a-custom-dialect-a-toy-example"},{"level":3,"text":"Step 1: Define the Dialect in TableGen","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#step-1-define-the-dialect-in-tablegen"},{"level":3,"text":"Step 2: Write a Domain-Specific Optimization Pass","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#step-2-write-a-domain-specific-optimization-pass"},{"level":3,"text":"Step 3: Lower to Standard Dialects","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#step-3-lower-to-standard-dialects"},{"level":2,"text":"The IR In Action","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#the-ir-in-action"},{"level":2,"text":"Real-World MLIR: The ML Compiler Landscape","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#real-world-mlir-the-ml-compiler-landscape"},{"level":3,"text":"IREE: Google's ML Runtime Built on MLIR","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#iree-googles-ml-runtime-built-on-mlir"},{"level":3,"text":"Mojo: A Language Built on MLIR","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#mojo-a-language-built-on-mlir"},{"level":2,"text":"Getting Started: The Practical Path","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#getting-started-the-practical-path"},{"level":3,"text":"1. Build MLIR from Source","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#1-build-mlir-from-source"},{"level":3,"text":"2. Play with mlir-opt","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#2-play-with-mlir-opt"},{"level":3,"text":"3. Study the Toy Tutorial","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#3-study-the-toy-tutorial"},{"level":3,"text":"4. Look at Real Dialects","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#4-look-at-real-dialects"},{"level":2,"text":"Why This Matters Beyond Compilers","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#why-this-matters-beyond-compilers"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#questions"},{"level":3,"text":"What is MLIR?","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-1"},{"level":3,"text":"How is MLIR different from LLVM IR?","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-2"},{"level":3,"text":"What are MLIR dialects?","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-3"},{"level":3,"text":"Who uses MLIR in production?","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-4"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#article-tips"}],"sections":[{"heading":"LLVM MLIR: Building Custom Compiler Dialects for Domain-Specific Optimization","level":1,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects","content":"Compilers"},{"heading":"LLVM MLIR: Building Custom Compiler Dialects for Domain-Specific Optimization","level":1,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#title","content":"2026-01-2711 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/llvm-mlir-compiler-infrastructure-custom-dialects>)"},{"heading":"The Problem: Why LLVM IR Wasn't Enough","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#the-problem-why-llvm-ir-wasnt-enough","content":"[LLVM](<https://llvm.org/>) is effective for what it does: take moderately low-level IR and turn it into optimized machine code for a dozen architectures. But it has a fundamental limitation, there's only one level of IR, and it sits at roughly the abstraction level of C.\n\nIf you're building a compiler for a high-level language (say, a tensor processing DSL), you have to lower your high-level concepts to LLVM IR early. Once you do, information is lost. The LLVM optimizer sees loops and pointer arithmetic, not matrix multiplications and convolutions. It can't fuse two matrix operations that it doesn't know are matrix operations.\n\nThis is the \"abstraction gap\" that plagued ML compilers for years:\n\n```\nHigh-level DSL: matmul(A, B) + transpose(C)\n │\n ┌───────────────┤ (information loss here)\n ▼ │\nLLVM IR: nested loops, gep instructions, loads/stores\n → LLVM can optimize the loops but can't see the linear algebra\n```\n\nEvery ML framework solved this differently: TensorFlow had XLA, PyTorch had TorchScript then Torch-MLIR, TVM had its own IR, Halide had its own IR. Each one reinvented type systems, SSA construction, pass infrastructure, and canonicalization. Massive duplication."},{"heading":"MLIR's Core Insight: Dialects All the Way Down","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#mlirs-core-insight-dialects-all-the-way-down","content":"MLIR's key insight is that you don't need one IR, you need a framework for building many IRs that can coexist and interoperate. That framework is the **dialect** system.\n\nA dialect is a namespace containing:\n\n- **Operations** (the \"instructions\" of your IR)\n- **Types** (the type system for your domain)\n- **Attributes** (compile-time constants and metadata)\n- **Interfaces** (traits that operations can implement for generic transformations)\n\nThe magic is that operations from different dialects can exist in the same IR, and you can progressively lower from high-level dialects to low-level ones:\n\n```mlir\n// High-level: linalg dialect (linear algebra operations)\n%result = linalg.matmul ins(%A, %B : tensor<64x128xf32>, tensor<128x256xf32>)\n outs(%C : tensor<64x256xf32>) -> tensor<64x256xf32>\n\n// Mid-level: affine dialect (polyhedral loop nests)\naffine.for %i = 0 to 64 {\n affine.for %j = 0 to 256 {\n affine.for %k = 0 to 128 {\n %a = affine.load %A[%i, %k] : memref<64x128xf32>\n %b = affine.load %B[%k, %j] : memref<128x256xf32>\n %c = affine.load %C[%i, %j] : memref<64x256xf32>\n %prod = arith.mulf %a, %b : f32\n %sum = arith.addf %c, %prod : f32\n affine.store %sum, %C[%i, %j] : memref<64x256xf32>\n }\n }\n}\n\n// Low-level: llvm dialect (LLVM IR operations)\n%ptr = llvm.getelementptr %base[%idx] : (!llvm.ptr, i64) -> !llvm.ptr\n%val = llvm.load %ptr : !llvm.ptr -> f32\n```\n\nAt each level, you can apply domain-specific optimizations that would be impossible at lower levels. The ` linalg ` level knows about tiling and fusion strategies for matrix operations. The ` affine ` level can apply polyhedral transformations. The ` llvm ` level does register allocation and instruction selection."},{"heading":"Building a Custom Dialect: A Toy Example","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#building-a-custom-dialect-a-toy-example","content":"The following example builds a simple dialect for a hypothetical image processing DSL. It defines operations for loading images, applying filters, and compositing."},{"heading":"Step 1: Define the Dialect in TableGen","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#step-1-define-the-dialect-in-tablegen","content":"```tablegen\n// ImageOps.td\ninclude \"mlir/IR/OpBase.td\"\ninclude \"mlir/IR/AttrTypeBase.td\"\ninclude \"mlir/Interfaces/SideEffectInterfaces.td\"\n\n// Define the dialect\ndef ImageDialect : Dialect {\n let name = \"image\";\n let cppNamespace = \"::image\";\n let summary = \"A dialect for image processing operations\";\n let description = [{\n The image dialect provides operations for loading, transforming,\n and compositing images with type-safe channel information.\n }];\n}\n\n// Define a custom type for images with channel info\ndef Image_ImageType : TypeDef<ImageDialect, \"Image\"> {\n let mnemonic = \"img\";\n let parameters = (ins\n \"int64_t\":$width,\n \"int64_t\":$height,\n \"int64_t\":$channels // 1=gray, 3=RGB, 4=RGBA\n );\n let assemblyFormat = \"`<` $width `,` $height `,` $channels `>`\";\n}\n\n// Load operation\ndef Image_LoadOp : Op<ImageDialect, \"load\", [Pure]> {\n let summary = \"Load an image from a path\";\n let arguments = (ins StrAttr:$path);\n let results = (outs Image_ImageType:$result);\n let assemblyFormat = \"$path attr-dict `:` type($result)\";\n}\n\n// Blur operation\ndef Image_BlurOp : Op<ImageDialect, \"blur\", [Pure]> {\n let summary = \"Apply Gaussian blur\";\n let arguments = (ins\n Image_ImageType:$input,\n F32Attr:$sigma\n );\n let results = (outs Image_ImageType:$result);\n let assemblyFormat = \"$input `,` $sigma attr-dict `:` type($input) `->` type($result)\";\n\n // Verify input and output have same dimensions\n let verifier = [{\n auto inType = getInput().getType().cast<ImageType>();\n auto outType = getResult().getType().cast<ImageType>();\n if (inType.getWidth() != outType.getWidth() ||\n inType.getHeight() != outType.getHeight())\n return emitOpError(\"input and output dimensions must match\");\n return success();\n }];\n}\n\n// Composite operation\ndef Image_CompositeOp : Op<ImageDialect, \"composite\", [Pure]> {\n let summary = \"Composite two images with blend mode\";\n let arguments = (ins\n Image_ImageType:$foreground,\n Image_ImageType:$background,\n StrAttr:$blend_mode // \"over\", \"multiply\", \"screen\"\n );\n let results = (outs Image_ImageType:$result);\n let assemblyFormat = [{\n $foreground `,` $background `,` $blend_mode attr-dict `:`\n type($foreground) `,` type($background) `->` type($result)\n }];\n}\n```"},{"heading":"Step 2: Write a Domain-Specific Optimization Pass","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#step-2-write-a-domain-specific-optimization-pass","content":"Now let's write a pass that fuses consecutive blur operations into a single blur with combined sigma:\n\n```cpp\n// FuseBlursPass.cpp\n#include \"mlir/Pass/Pass.h\"\n#include \"ImageOps.h\"\n#include <cmath>\n\nnamespace {\nstruct FuseBlursPass\n : public mlir::PassWrapper<FuseBlursPass,\n mlir::OperationPass<mlir::func::FuncOp>> {\n\n void runOnOperation() override {\n getOperation().walk([](image::BlurOp outerBlur) {\n // Check if the input to this blur is another blur\n auto innerBlur = outerBlur.getInput().getDefiningOp<image::BlurOp>();\n if (!innerBlur)\n return;\n\n // Check that the inner blur has no other users\n if (!innerBlur.getResult().hasOneUse())\n return;\n\n // Fuse: sigma_combined = sqrt(sigma1^2 + sigma2^2)\n float sigma1 = innerBlur.getSigma().convertToFloat();\n float sigma2 = outerBlur.getSigma().convertToFloat();\n float fusedSigma = std::sqrt(sigma1 * sigma1 + sigma2 * sigma2);\n\n // Replace the outer blur with a single blur on the inner's input\n mlir::OpBuilder builder(outerBlur);\n auto fusedBlur = builder.create<image::BlurOp>(\n outerBlur.getLoc(),\n outerBlur.getType(),\n innerBlur.getInput(),\n builder.getF32FloatAttr(fusedSigma));\n\n outerBlur.replaceAllUsesWith(fusedBlur.getResult());\n outerBlur.erase();\n if (innerBlur.getResult().use_empty())\n innerBlur.erase();\n });\n }\n\n mlir::StringRef getArgument() const final { return \"image-fuse-blurs\"; }\n mlir::StringRef getDescription() const final {\n return \"Fuse consecutive blur operations\";\n }\n};\n} // namespace\n\n// Register the pass\nstd::unique_ptr<mlir::Pass> createFuseBlursPass() {\n return std::make_unique<FuseBlursPass>();\n}\n```"},{"heading":"Step 3: Lower to Standard Dialects","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#step-3-lower-to-standard-dialects","content":"Eventually, we need to lower our image operations to something LLVM can compile. Here's a sketch of lowering ` image.blur ` to loop nests:\n\n```cpp\nstruct BlurOpLowering : public mlir::OpConversionPattern<image::BlurOp> {\n using OpConversionPattern::OpConversionPattern;\n\n mlir::LogicalResult matchAndRewrite(\n image::BlurOp op,\n OpAdaptor adaptor,\n mlir::ConversionPatternRewriter &rewriter) const override {\n\n auto loc = op.getLoc();\n auto imgType = op.getInput().getType().cast<image::ImageType>();\n int64_t width = imgType.getWidth();\n int64_t height = imgType.getHeight();\n int64_t channels = imgType.getChannels();\n float sigma = op.getSigma().convertToFloat();\n\n // Compute kernel size from sigma\n int kernelSize = static_cast<int>(std::ceil(sigma * 3.0f)) * 2 + 1;\n int halfKernel = kernelSize / 2;\n\n // Allocate output buffer\n auto memrefType = mlir::MemRefType::get(\n {height, width, channels}, rewriter.getF32Type());\n auto output = rewriter.create<mlir::memref::AllocOp>(loc, memrefType);\n\n // Generate nested loops for convolution\n // (simplified, real implementation would generate separable passes)\n auto yLoop = rewriter.create<mlir::affine::AffineForOp>(\n loc, 0, height, 1);\n rewriter.setInsertionPointToStart(yLoop.getBody());\n\n auto xLoop = rewriter.create<mlir::affine::AffineForOp>(\n loc, 0, width, 1);\n rewriter.setInsertionPointToStart(xLoop.getBody());\n\n // ... kernel computation loop body ...\n\n rewriter.replaceOp(op, output);\n return mlir::success();\n }\n};\n```"},{"heading":"The IR In Action","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#the-ir-in-action","content":"Here's what a complete pipeline looks like in our image dialect:\n\n```mlir\n// Input IR (high-level image operations)\nfunc.func @process_photo(%path: !image.img<1920, 1080, 3>) -> !image.img<1920, 1080, 3> {\n %photo = image.load \"/photos/input.jpg\" : !image.img<1920, 1080, 3>\n %blurred1 = image.blur %photo, 2.0 : !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n %blurred2 = image.blur %blurred1, 3.0 : !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n %overlay = image.load \"/photos/overlay.png\" : !image.img<1920, 1080, 4>\n %result = image.composite %overlay, %blurred2, \"over\" :\n !image.img<1920, 1080, 4>, !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n return %result : !image.img<1920, 1080, 3>\n}\n\n// After blur fusion pass (sigma = sqrt(4 + 9) = 3.606):\nfunc.func @process_photo(%path: !image.img<1920, 1080, 3>) -> !image.img<1920, 1080, 3> {\n %photo = image.load \"/photos/input.jpg\" : !image.img<1920, 1080, 3>\n %blurred = image.blur %photo, 3.606 : !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n %overlay = image.load \"/photos/overlay.png\" : !image.img<1920, 1080, 4>\n %result = image.composite %overlay, %blurred, \"over\" :\n !image.img<1920, 1080, 4>, !image.img<1920, 1080, 3> -> !image.img<1920, 1080, 3>\n return %result : !image.img<1920, 1080, 3>\n}\n```\n\nThe fusion pass turned two blur operations into one, cutting the convolution work roughly in half. This optimization is trivial to express at the image dialect level, but would be nearly impossible to discover after lowering to loops."},{"heading":"Real-World MLIR: The ML Compiler Landscape","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#real-world-mlir-the-ml-compiler-landscape","content":"The biggest users of MLIR are ML compilers, and for good reason. ML workloads have a natural multi-level structure:\n\n```\nFramework level: torch.nn.Linear(256, 128)\n │\n ▼ TorchMLIR\nGraph level: linalg.matmul + linalg.add (bias)\n │\n ▼ Tiling + Fusion\nTile level: scf.for loops with vectorized inner bodies\n │\n ▼ Hardware lowering\nHardware level: gpu.launch_func / vector.contract / amx.tile_mulf\n │\n ▼ LLVM lowering\nMachine code: AVX-512 / CUDA PTX / AMX instructions\n```\n\nEach level has its own optimization opportunities:\n\n- **Graph level**: operator fusion (matmul + bias + relu = one kernel), layout optimization (NHWC vs NCHW)\n- **Tile level**: tile size selection, cache-aware scheduling, vectorization strategy\n- **Hardware level**: mapping to specific ISA extensions (AMX for Intel, Tensor Cores for NVIDIA)"},{"heading":"IREE: Google's ML Runtime Built on MLIR","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#iree-googles-ml-runtime-built-on-mlir","content":"IREE (Intermediate Representation Execution Environment) is probably the most complete MLIR-based ML compiler in production. It takes TensorFlow, PyTorch, or JAX models, lowers them through MLIR dialects, and generates optimized code for CPUs, GPUs, and accelerators.\n\nThe compilation pipeline:\n\n```\nInput: StableHLO (from JAX/TF) or Torch-MLIR (from PyTorch)\n → IREE Input dialect (normalize all inputs)\n → IREE Flow dialect (partition into dispatches, manage buffers)\n → IREE Stream dialect (async execution scheduling)\n → IREE HAL dialect (hardware abstraction)\n → Target-specific codegen (LLVM CPU, SPIR-V, CUDA, Metal)\n```\n\nEach arrow is a series of MLIR passes that lower and optimize. The whole pipeline is around 80 passes, and every intermediate state is printable, verifiable MLIR."},{"heading":"Mojo: A Language Built on MLIR","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#mojo-a-language-built-on-mlir","content":"Modular's Mojo programming language is built directly on MLIR. This means Mojo's compiler can express high-level abstractions (Python-like syntax) and low-level hardware operations (SIMD intrinsics) in the same IR, and the optimization pipeline can see both simultaneously.\n\n```mojo\n# Mojo code that compiles through MLIR\nfn matmul_tiled[M: Int, N: Int, K: Int](\n A: SIMD[DType.float32, M*K],\n B: SIMD[DType.float32, K*N],\n) -> SIMD[DType.float32, M*N]:\n # This compiles to MLIR linalg ops, then gets tiled and vectorized\n # through the standard MLIR pipeline\n var C = SIMD[DType.float32, M*N](0)\n for i in range(M):\n for j in range(N):\n for k in range(K):\n C[i*N + j] += A[i*K + k] * B[k*N + j]\n return C\n```\n\nMojo's compiler sees the ` SIMD ` types and loop structure, matches it against tiling patterns, and generates code that uses hardware vector instructions directly. The MLIR infrastructure handles the type checking, SSA construction, and pass scheduling, Mojo's compiler team focuses on the domain-specific patterns."},{"heading":"Getting Started: The Practical Path","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#getting-started-the-practical-path","content":"For those wanting to experiment with MLIR, here is a recommended path:"},{"heading":"1. Build MLIR from Source","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#1-build-mlir-from-source","content":"```bash\ngit clone https://github.com/llvm/llvm-project.git\ncd llvm-project\nmkdir build && cd build\n\ncmake -G Ninja ../llvm \\\n -DLLVM_ENABLE_PROJECTS=mlir \\\n -DLLVM_BUILD_EXAMPLES=ON \\\n -DLLVM_TARGETS_TO_BUILD=\"host\" \\\n -DCMAKE_BUILD_TYPE=Release \\\n -DLLVM_ENABLE_ASSERTIONS=ON\n\nninja -j$(nproc)\n```"},{"heading":"2. Play with mlir-opt","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#2-play-with-mlir-opt","content":"```bash\n# Write a simple MLIR program\ncat > test.mlir << 'EOF'\nfunc.func @add(%arg0: f32, %arg1: f32) -> f32 {\n %0 = arith.addf %arg0, %arg1 : f32\n return %0 : f32\n}\nEOF\n\n# Run canonicalization\n./bin/mlir-opt --canonicalize test.mlir\n\n# Lower to LLVM dialect\n./bin/mlir-opt --convert-func-to-llvm --convert-arith-to-llvm test.mlir\n\n# See all available passes\n./bin/mlir-opt --help | grep \"Passes:\"\n```"},{"heading":"3. Study the Toy Tutorial","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#3-study-the-toy-tutorial","content":"The MLIR repo includes a complete tutorial that builds a compiler for a toy language from scratch. It's in ` mlir/docs/Tutorials/Toy/ ` and covers everything from lexing to LLVM codegen through MLIR."},{"heading":"4. Look at Real Dialects","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#4-look-at-real-dialects","content":"Study the ` linalg ` and ` affine ` dialect implementations in the MLIR source tree. They're well-documented and demonstrate production patterns for op definitions, transformations, and lowering."},{"heading":"Why This Matters Beyond Compilers","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#why-this-matters-beyond-compilers","content":"MLIR's dialect system is genuinely useful outside traditional compiler work:\n\n- **Hardware design**: CIRCT (Circuit IR Compilers and Tools) uses MLIR dialects to represent hardware description languages, replacing Verilog/VHDL toolchains.\n- **Quantum computing**: The ` quake ` dialect represents quantum circuits for optimization before mapping to quantum hardware.\n- **Database engines**: Query planners can use custom dialects to represent relational algebra and optimize execution plans.\n- **Numerical simulation**: The ` sparse_tensor ` dialect handles sparse matrix operations with automatic format selection.\n\nThe pattern is always the same: you have a domain with specific operations and optimization rules that are invisible at a lower abstraction level. MLIR gives you the infrastructure to express that domain without building everything from scratch.\n\nMLIR isn't just a better LLVM IR. It's a framework for building compilers at any abstraction level, with infrastructure that took decades to develop in LLVM, available out of the box. If you're building anything that transforms structured representations, whether that's ML models, hardware descriptions, or database queries, MLIR is worth your time."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#questions","content":""},{"heading":"What is MLIR?","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-1","content":"MLIR (Multi-Level Intermediate Representation) is a compiler infrastructure within the LLVM project that provides a flexible, extensible framework for building domain-specific compilers using composable 'dialects', collections of types, operations, and transformation rules."},{"heading":"How is MLIR different from LLVM IR?","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-2","content":"LLVM IR is a single, fixed-level representation designed for general-purpose CPU code generation. MLIR supports multiple abstraction levels simultaneously, letting you represent high-level domain concepts (tensor operations, hardware intrinsics) alongside low-level code."},{"heading":"What are MLIR dialects?","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-3","content":"Dialects are modular collections of operations, types, and attributes in MLIR. Each dialect defines its own vocabulary, for example, the 'linalg' dialect has operations for linear algebra, while the 'gpu' dialect has operations for GPU kernels. You can mix dialects freely in the same IR."},{"heading":"Who uses MLIR in production?","level":3,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#question-4","content":"Google (TensorFlow/XLA, IREE), AMD (ROCm compiler stack), Intel (oneAPI), Modular (Mojo language), and several ML hardware startups use MLIR as their compiler backbone."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#article-tips","content":"1. **Tip:** When defining custom MLIR ops, always use TableGen (` .td ` files) instead of hand-writing C++ op classes. TableGen auto-generates verifiers, builders, parsers, and printers. Writing ops by hand is a debugging nightmare.\n2. **Tip:** MLIR's ` -mlir-print-ir-after-all ` flag dumps the IR after every pass. Pipe it through ` diff ` to see exactly what each transformation does. This is the single most useful debugging technique for pass development.\n3. **Tip:** Start with the ` func ` and ` arith ` dialects when learning MLIR. They're simple enough to understand in an afternoon but demonstrate all the core concepts, regions, blocks, SSA values, and type systems.\n4. **Tip:** Use ` mlir-opt --canonicalize ` as your first optimization pass. Canonicalization patterns (constant folding, dead code elimination, identity removal) are registered per-op and run to fixpoint. It catches 80% of obvious redundancies.\n\n- llvm\n- mlir\n- compilers\n- dialects\n- optimization\n- machine-learning\n- dsl\n- intermediate-representation\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/llvm-mlir-compiler-infrastructure-custom-dialects"},{"text":"LLVM","url":"https://llvm.org/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects"},"headline":"LLVM MLIR: Building Custom Compiler Dialects for Domain-Specific Optimization","description":"An examination of MLIR's multi-level intermediate representation architecture, custom dialect construction, and the convergence of ML compilers, hardware accelerators, and DSLs on the MLIR framework.","datePublished":"2026-01-27","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Compilers","keywords":"llvm, mlir, compilers, dialects, optimization, machine-learning, dsl, intermediate-representation","wordCount":2019,"timeRequired":"PT11M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/llvm-mlir-compiler-infrastructure-custom-dialects"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"LLVM MLIR: Building Custom Compiler Dialects for Domain-Specific Optimization","item":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is MLIR?","acceptedAnswer":{"@type":"Answer","text":"MLIR (Multi-Level Intermediate Representation) is a compiler infrastructure within the LLVM project that provides a flexible, extensible framework for building domain-specific compilers using composable 'dialects', collections of types, operations, and transformation rules."}},{"@type":"Question","name":"How is MLIR different from LLVM IR?","acceptedAnswer":{"@type":"Answer","text":"LLVM IR is a single, fixed-level representation designed for general-purpose CPU code generation. MLIR supports multiple abstraction levels simultaneously, letting you represent high-level domain concepts (tensor operations, hardware intrinsics) alongside low-level code."}},{"@type":"Question","name":"What are MLIR dialects?","acceptedAnswer":{"@type":"Answer","text":"Dialects are modular collections of operations, types, and attributes in MLIR. Each dialect defines its own vocabulary, for example, the 'linalg' dialect has operations for linear algebra, while the 'gpu' dialect has operations for GPU kernels. You can mix dialects freely in the same IR."}},{"@type":"Question","name":"Who uses MLIR in production?","acceptedAnswer":{"@type":"Answer","text":"Google (TensorFlow/XLA, IREE), AMD (ROCm compiler stack), Intel (oneAPI), Modular (Mojo language), and several ML hardware startups use MLIR as their compiler backbone."}}]}]}],"contentHash":"937d0e41a8afc754f68c747ea0aeca70090b1b8cc1a6d1837dded647693673db","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/llvm-mlir-compiler-infrastructure-custom-dialects","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":81013,"contentBytes":18121}}
