| # Remark Infrastructure | |
| Remarks are **structured, human- and machine-readable notes** emitted by the | |
| compiler to communicate: | |
| - What transformations were applied | |
| - What optimizations were missed | |
| - Why certain decisions were made | |
| The **`RemarkEngine`** collects remarks during compilation and routes them to a | |
| pluggable **streamer**. By default, MLIR integrates with LLVM's | |
| [`llvm::remarks`](https://llvm.org/docs/Remarks.html) infrastructure, enabling | |
| you to: | |
| - Stream remarks as passes run | |
| - Serialize to **YAML** or **LLVM Bitstream** | |
| *** | |
| ## Overview | |
| - **Opt-in** – Disabled by default; zero overhead unless enabled. | |
| - **Per-context** – Configured on `MLIRContext`. | |
| - **Formats** – LLVM Remark engine (YAML / Bitstream) or custom streamers. | |
| - **Kinds** – `Passed`, `Missed`, `Failure`, `Analysis`. | |
| - **API** – Lightweight streaming interface using `<<` (like MLIR diagnostics). | |
| *** | |
| ## Architecture | |
| The remark system consists of two main components: | |
| ### RemarkEngine | |
| Owned by `MLIRContext`, the engine: | |
| - Receives finalized `InFlightRemark` objects | |
| - Optionally mirrors remarks to the `DiagnosticEngine` | |
| - Dispatches to the installed streamer | |
| ### MLIRRemarkStreamerBase | |
| An abstract backend interface with a single hook: | |
| ```c++ | |
| virtual void streamOptimizationRemark(const Remark &remark) = 0; | |
| ``` | |
| The default implementation, **`MLIRLLVMRemarkStreamer`**, adapts `mlir::Remark` | |
| to LLVM's remark format and writes YAML or Bitstream via | |
| `llvm::remarks::RemarkStreamer`. | |
| **Ownership chain:** `MLIRContext` → `RemarkEngine` → `MLIRRemarkStreamerBase` | |
| *** | |
| ## Remark Categories | |
| MLIR provides four built-in categories: | |
| ### Passed | |
| An optimization or transformation succeeded. | |
| ``` | |
| [Passed] RemarkName | Category:Vectorizer:myPass1 | Function=foo | Remark="vectorized loop", tripCount=128 | |
| ``` | |
| ### Missed | |
| An optimization didn't apply and produces ideally an actionable feedback. | |
| ``` | |
| [Missed] | Category:Unroll | Function=foo | Reason="tripCount=4 < threshold=256", Suggestion="increase unroll to 128" | |
| ``` | |
| ### Failure | |
| An optimization was attempted but failed. Unlike `Missed`, this indicates an | |
| active attempt that couldn't complete. | |
| For example, when a user requests `--use-max-register=100` but the allocator | |
| cannot satisfy the constraint: | |
| ``` | |
| [Failed] Category:RegisterAllocator | Reason="Limiting to use-max-register=100 failed; it now uses 104 registers for better performance" | |
| ``` | |
| ### Analysis | |
| Neutral informational output—useful for profiling and debugging. | |
| ``` | |
| [Analysis] Category:Register | Remark="Kernel uses 168 registers" | |
| [Analysis] Category:Register | Remark="Kernel uses 10kB local memory" | |
| ``` | |
| *** | |
| ## Emitting Remarks | |
| Use the `remark::*` helpers to create an **in-flight remark**, then append | |
| content with the `<<` operator. | |
| ### Configuring Remark Options | |
| Each remark accepts four fields (all `StringRef`): | |
| | Field | Description | | |
| |***************-|************************************************| | |
| | **Name** | Identifiable name for the remark | | |
| | **Category** | High-level classification | | |
| | **Sub-category** | Fine-grained classification | | |
| | **Function** | The function where the remark originates | | |
| ### Basic Example | |
| ```c++ | |
| #include "mlir/IR/Remarks.h" | |
| LogicalResult MyPass::runOnOperation() { | |
| Location loc = getOperation()->getLoc(); | |
| auto opts = remark::RemarkOpts::name("VectorizeLoop") | |
| .category("Vectorizer") | |
| .subCategory("MyPass") | |
| .function("foo"); | |
| // Passed: transformation succeeded | |
| remark::passed(loc, opts) | |
| << "vectorized loop" | |
| << remark::metric("tripCount", 128); | |
| // Analysis: informational output | |
| remark::analysis(loc, opts) | |
| << "Kernel uses 168 registers"; | |
| // Missed: optimization skipped (with reason and suggestion) | |
| remark::missed(loc, opts) | |
| << remark::reason("tripCount={0} < threshold={1}", 4, 256) | |
| << remark::suggest("increase unroll factor to {0}", 128); | |
| // Failure: optimization attempted but failed | |
| remark::failed(loc, opts) | |
| << remark::reason("unsupported pattern encountered"); | |
| return success(); | |
| } | |
| ``` | |
| *** | |
| ## Metrics and Helpers | |
| All helper functions accept | |
| [LLVM format strings](https://llvm.org/docs/ProgrammersManual.html#formatting-strings-the-formatv-function), | |
| which build lazily—ensuring zero cost when remarks are disabled. | |
| | Helper | Description | | |
| |******************************--|******************************************| | |
| | `remark::metric(key, value)` | Adds a structured key–value pair | | |
| | `remark::add(fmt, ...)` | Shortcut for `metric("Remark", ...)` | | |
| | `remark::reason(fmt, ...)` | Shortcut for `metric("Reason", ...)` | | |
| | `remark::suggest(fmt, ...)` | Shortcut for `metric("Suggestion", ...)` | | |
| ### String Shorthand | |
| Appending a plain string: | |
| ```c++ | |
| remark::passed(loc, opts) << "vectorized loop"; | |
| ``` | |
| is equivalent to: | |
| ```c++ | |
| remark::passed(loc, opts) << remark::metric("Remark", "vectorized loop"); | |
| ``` | |
| ### Custom Metrics | |
| Add structured data for machine readability: | |
| ```c++ | |
| remark::passed(loc, opts) | |
| << "loop optimized" | |
| << remark::metric("TripCount", 128) | |
| << remark::metric("VectorWidth", 4); | |
| ``` | |
| *** | |
| ## Emitting Policies | |
| The `RemarkEngine` supports pluggable policies that control which remarks are | |
| emitted. | |
| ### RemarkEmittingPolicyAll | |
| Emits **all** remarks unconditionally. | |
| ### RemarkEmittingPolicyFinal | |
| Stores remarks until `finalize()` is called and emits only the **final** remark | |
| for each location. This is useful in multi-pass compilers where an early pass | |
| may report a failure, but a later pass succeeds. `finalize()` drains the stored | |
| remarks and emits them in the order in which they were created. Calling it | |
| again emits only remarks reported since. | |
| **Example:** Only the successful remark is emitted: | |
| ```c++ | |
| auto opts = remark::RemarkOpts::name("Unroller").category("LoopUnroll"); | |
| // First pass: reports failure | |
| remark::failed(loc, opts) << "Loop could not be unrolled"; | |
| // Later pass: reports success (this is the one emitted) | |
| remark::passed(loc, opts) << "Loop unrolled successfully"; | |
| ``` | |
| You can also implement custom policies by inheriting from the policy interface. | |
| *** | |
| ## Querying Enabled Remarks | |
| A remark is only built when its kind and category match the configured | |
| filters. The `remark::passed`, `remark::missed`, `remark::failed` and | |
| `remark::analysis` helpers perform this check themselves, so passes do not need | |
| to do anything. Code that produces remarks from another source, for example a | |
| bridge that imports remarks from a different compiler, can ask the engine what | |
| is enabled before doing any work: | |
| ```c++ | |
| remark::detail::RemarkEngine *engine = context.getRemarkEngine(); | |
| if (!engine || !engine->isAnyRemarkEnabled()) | |
| return; // No remark engine, or no category filter is active. | |
| if (engine->isRemarkEnabled(remark::RemarkKind::RemarkPassed, "Vectorizer")) | |
| remark::passed(loc, opts) << "vectorized loop"; | |
| ``` | |
| | Query | Answers | | |
| |----------------------------------------------|------------------------------------------------------| | |
| | `isAnyRemarkEnabled()` | At least one category filter is active | | |
| | `isAnyRemarkEnabled(category)` | Some kind of remark is enabled for the category | | |
| | `isRemarkEnabled(kind, category)` | Remarks of `kind` are enabled for the category | | |
| | `is{Passed,Missed,Analysis,Failed}OptRemarkEnabled(category)` | Per-kind query | | |
| *** | |
| ## Enabling Remarks | |
| ### Option 1: LLVM Remark Streamer (YAML or Bitstream) | |
| Persist remarks to a file for post-processing: | |
| ```c++ | |
| // Setup categories | |
| remark::RemarkCategories cats{ | |
| /*passed=*/ "LoopUnroll", | |
| /*missed=*/ std::nullopt, | |
| /*analysis=*/ std::nullopt, | |
| /*failed=*/ "LoopUnroll" | |
| }; | |
| // Use final policy | |
| std::unique_ptr<remark::RemarkEmittingPolicyFinal> policy = | |
| std::make_unique<remark::RemarkEmittingPolicyFinal>(); | |
| remark::enableOptimizationRemarksWithLLVMStreamer( | |
| context, outputFile, llvm::remarks::Format::YAML, std::move(policy), cats); | |
| ``` | |
| **YAML output** (human-readable): | |
| ```yaml | |
| *** !Passed | |
| pass: Vectorizer:MyPass | |
| name: VectorizeLoop | |
| function: foo | |
| loc: input.mlir:12:3 | |
| args: | |
| - Remark: vectorized loop | |
| - tripCount: 128 | |
| ``` | |
| **Bitstream format** — compact binary for large-scale analysis. | |
| ### Option 2: Diagnostic Engine (No Streamer) | |
| Mirror remarks to the standard diagnostic output: | |
| ```c++ | |
| // Setup categories | |
| remark::RemarkCategories cats{ | |
| /*passed=*/ "LoopUnroll", | |
| /*missed=*/ std::nullopt, | |
| /*analysis=*/ std::nullopt, | |
| /*failed=*/ "LoopUnroll" | |
| }; | |
| // Use final policy | |
| std::unique_ptr<remark::RemarkEmittingPolicyFinal> policy = | |
| std::make_unique<remark::RemarkEmittingPolicyFinal>(); | |
| remark::enableOptimizationRemarks( | |
| context, | |
| /*streamer=*/ nullptr, | |
| /*policy=*/ std::move(policy), | |
| cats, | |
| /*printAsEmitRemarks=*/ true); | |
| ``` | |
| ### Option 3: Custom Streamer | |
| Implement your own backend for specialized output formats: | |
| ```c++ | |
| class MyStreamer : public MLIRRemarkStreamerBase { | |
| public: | |
| void streamOptimizationRemark(const Remark &remark) override { | |
| // Custom serialization logic | |
| } | |
| }; | |
| auto streamer = std::make_unique<MyStreamer>(); | |
| remark::enableOptimizationRemarks(context, std::move(streamer), cats); | |
| ``` | |
| *** | |
| ## Importing LLVM Remarks | |
| LLVM passes emit their own | |
| [optimization remarks](https://llvm.org/docs/Remarks.html) while MLIR compiles | |
| a module with LLVM, for example in `gpu-module-to-binary`. `ModuleToObject` | |
| installs a `remark::LLVMToMLIRDiagnosticHandler` on the `llvm::LLVMContext` it | |
| compiles with, which imports these remarks into the remark engine: the category | |
| is the LLVM pass name prefixed with `llvm-`, the LLVM remark name becomes the | |
| name, the message is stored under the `Remark` argument and the structured LLVM | |
| arguments are copied as metrics. The remark filters therefore select LLVM | |
| remarks by pass name, and `llvm-.*` selects all of them: | |
| ``` | |
| mlir-opt kernel.mlir --gpu-module-to-binary="format=isa" --remarks-filter-passed=llvm-inline | |
| ``` | |
| ``` | |
| [Passed] Inlined | Category:llvm-inline | Function=kernel | Callee=helper, Caller=kernel, Cost=-30, Remark="'helper' inlined into 'kernel' with (cost=-30, threshold=337)", Threshold=337 | |
| ``` | |
| Remarks that are not enabled in the engine are left to LLVM, so LLVM's | |
| `-pass-remarks` flags keep working. Other LLVM diagnostics, such as errors | |
| reported by a backend, become MLIR diagnostics. | |
| LLVM remarks carry a source location only when the LLVM IR has debug | |
| information; run `ensure-debug-info-scope-on-llvm-func` before serializing to | |
| get `file:line:column` locations. Otherwise a remark is attached to the MLIR | |
| symbol named after its LLVM function, or to the serialized operation. | |
| Remarks saved by an LLVM-based compiler to a YAML or bitstream file can be | |
| replayed into the engine with `remark::importLLVMRemarks`, which applies the | |
| same conversion. |