MLIR dialectをDocker image化してインストール不要にする

# TL; DR

https://github.com/Syuparn/mlir-hello

```bash
# イメージが大きいので注意！(2GB)
$ docker run -it --rm ghcr.io/syuparn/mlir-hello:main bash
root@032a4b2bb9d6:/mlir-hello# ./build/bin/hello-opt ./test/Hello/print.mlir > print.ll
root@032a4b2bb9d6:/mlir-hello# /lib/llvm-18/bin/lli print.ll
1.000000 2.000000 3.000000 
4.000000 5.000000 6.000000
```

（上記はこちらのプロジェクトをforkして作成）

https://github.com/Lewuathe/mlir-hello

# はじめに

MLIR（Multi-Level Intermediate Representation）は、言語の中間表現や中間表現同士の変換等を定義することができるコンパイラ基盤です。

LLVMプロジェクトの1つとして管理されており、LLVM IRよりASTに近い抽象的な表現(dialect)を定義することも可能です（徐々に抽象度の低い表現へ変換する（lowering）ことでLLVM IRに変換します）。

MLIRについては、こちらの記事が詳しく紹介されています。

https://zenn.dev/acd1034/articles/230423-mlir3vdt

そんなMLIRを現在学習中なのですが、実装以前に環境構築でかなり躓いてしまいました。

チュートリアルである `Toy` 言語の実装では、動かす前にLLVM/MLIRをソースコードからビルドします。
オプションやツール同士のバージョンに気を付ける必要があり、さらに（自業自得ですが）事前にLLVM 17もインストールしていたため[^llvm-17]環境がぐちゃぐちゃになってしまいました。

https://mlir.llvm.org/docs/Tutorials/Toy/

MLIRで自作言語を作る予定でしたが、利用者が上記のようにコンパイラをビルドする必要があるとなるとHello Worldの前に心が折れてしまいそうです。準備不要で、1コマンドでコンパイラを使い始められる方法があれば...

ありました。**Dockerコンテナ**です。

そこで本記事では、ビルド済みMLIR dialectのDockerイメージを作る方法を紹介します。

## 目標
趣味の自作言語を布教しやすくする（手軽に触ってもらえるようにする）のが目標です。また、開発者（自分）自身も手軽に環境構築できるのが理想です。

- 利用者：Dockerコンテナ
    - イメージをpullしてコンテナを起動するだけでコンパイラを実行できる
- 開発者：ローカル環境（Ubuntu）
    - apt installだけで依存ツールが揃う
    - VSCodeのインテリセンスも効く

代わりに、イメージサイズや動作の効率は不問としました。~~実際2GBを超えています~~

# バージョン

- LLVM/MLIR 18.1.4
- ローカル環境: Ubuntu 20.04 (on WSL2)

# ビルドするプロジェクト

[llvm-project](https://github.com/llvm/llvm-project) のリポジトリと独立している小さなdialectが必要だったので、こちらのHello Dialectをforkして使わせていただきました。

https://github.com/Lewuathe/mlir-hello

コンテナ内でも、サンプルコードの以下のMLIRをLLVM IRへ変換できるようにします。

```mlir:test/Hello/print.mlir
func.func @main() {
    %0 = "hello.constant"() {value = dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>} : () -> tensor<2x3xf64>
    "hello.print"(%0) : (tensor<2x3xf64>) -> ()
    return
}
```

Dialectについての詳細は作者様の解説記事をご覧ください。

https://note.com/lewuathe/n/na64b95954988

# 必要なツールをインストールする
イメージ化にあたり、[llvm-project](https://github.com/llvm/llvm-project)からビルドしていた[^project]ツールを別手段で入手します。

Ubuntuのベースイメージを使用すればローカル環境と同じコマンドでイメージも作成できるため、まずはローカル環境の構築について見ていきます。

## LLVM, MLIRをaptでインストールする
最新版（執筆時点）のLLVM 18.1.4をインストールします。公式サイトの手順に従いリポジトリを追加します。

debパッケージが

- Ubuntuのバージョン
- LLVMのメジャーバージョン

ごとに異なる点に注意です。

https://apt.llvm.org/

```bash
# 例：Ubuntu22.04 / LLVM, MLIR 18の場合
$ wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -
$ apt-add-repository "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main"
$ apt update
$ apt install -y libllvm-18-ocaml-dev libllvm18 llvm-18 llvm-18-dev llvm-18-doc llvm-18-examples llvm-18-runtime libmlir-18-dev libmlir-18 mlir-18-tools
```

## llvm-litをインストールする

上記ではe2eテストに使用する `llvm-lit` がインストールできないため、pip経由で別途インストールします。現状1つ前のパッチバージョン `18.1.3` しか公開されていませんでしたが、互換性は問題ありませんでした。

```bash
$ pip install lit==18.1.3
```

mlir-helloでは `llvm-lit` の実行ファイルを `./build/bin` 配下に置く必要があるので、突貫工事ですが以下のbashファイルで `lit` コマンドを呼び出すようにしています。

```bash:bin/llvm-lit
#!/bin/bash

lit $@
```

## 動作確認

ここまでで、ローカル環境でdialectのビルド、テストがどちらも成功するようになりました。

```bash
# dialectのビルド
cd build
# 注意：aptでLLVM, MLIRをインストールしたディレクトリを指定すること！
cmake -G Ninja .. -DLLVM_DIR=/lib/llvm-18/lib/cmake/llvm   -DMLIR_DIR=/lib/llvm-18/lib/cmake/mlir
cmake --build . --target hello-opt

# コンパイラの動作確認
$ cd ..
# Hello dialect: MLIR -> LLVM IR
$ ./build/bin/hello-opt ./test/Hello/print.mlir > print.ll
# LLVM IRのインタープリタで実行
$ lli print.ll
1.000000 2.000000 3.000000
4.000000 5.000000 6.000000
```

```bash
# テスト
$ cd build
$ cmake --build . --target check-hello
[0/1] Running the hello regression tests

Testing Time: 0.04s

Total Discovered Tests: 9
  Passed: 9 (100.00%)
```

## イメージ化する

イメージのビルドについても、基本的には上記コマンドを `Dockerfile` に記載するだけです。

```Dockerfile
FROM ubuntu:22.04

# cmake ninja-build: ビルドに必要
# software-properties-common: apt-add-repositoryに必要
RUN apt-get update -y && apt-get install -y python3 python3-pip cmake ninja-build curl wget software-properties-common

# LLVM/MLIR 18 のインストール
RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -
RUN apt-add-repository "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" && \
    apt-get update  && \
    apt-get install -y libllvm-18-ocaml-dev libllvm18 llvm-18 llvm-18-dev llvm-18-doc llvm-18-examples llvm-18-runtime libmlir-18-dev libmlir-18 mlir-18-tools

# llvm-lit のインストール
RUN pip install lit==18.1.3

# Hello dialectをビルド
WORKDIR /mlir-hello
COPY . .
COPY llvm-lit build/bin/llvm-lit

WORKDIR /mlir-hello/build
RUN cmake -G Ninja .. -DLLVM_DIR=/lib/llvm-18/lib/cmake/llvm   -DMLIR_DIR=/lib/llvm-18/lib/cmake/mlir
RUN cmake --build . --target hello-opt

WORKDIR /mlir-hello
```

`apt-add-repository` についてはこちらの記事を参考にさせていただきました。

https://itsfoss.com/add-apt-repository-command-not-found/

あとはghcr.ioにpushすれば、利用者は `docker run` だけでコンパイラが使用できるようになりました！

```bash
# イメージが大きいので注意！(2GB越え)
$ docker run -it --rm ghcr.io/syuparn/mlir-hello:main bash
root@032a4b2bb9d6:/mlir-hello# ./build/bin/hello-opt ./test/Hello/print.mlir > print.ll
root@032a4b2bb9d6:/mlir-hello# /lib/llvm-18/bin/lli print.ll
1.000000 2.000000 3.000000 
4.000000 5.000000 6.000000
```

GitHub Actionsは[公式ドキュメント](https://docs.github.com/ja/enterprise-cloud@latest/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions)を参考に作成しました。

https://github.com/Syuparn/mlir-hello/blob/main/.github/workflows/build.yml

# 開発環境の整備
続いて、開発者側のローカル環境を整備します。コンパイルは前章まででできるようになりましたが、VSCode上でclangdのインテリセンスが効かなくなってしまったので数か所対応が必要でした。

## clangdのインテリセンスが効くようにする
### includeしたヘッダファイルが見つからない問題
aptでインストールしたものとは別のLLVMを参照してしまっていたため、一部の `include` でnot foundが発生してしまいました。

clangdにもビルド時と同じコンパイルオプションを認識させるため、`compile_flags.txt` をリポジトリのルートフォルダに追加することで解消しました。

```:compile_flags.txt
-xc++
-Iinclude
-DLLVM_DIR=/lib/llvm-18/lib/cmake/llvm
-DMLIR_DIR=/lib/llvm-18/lib/cmake/mlir
```

`compile_flags.txt` についてはこちらの記事を参考にさせていただきました。

https://uchan.hateblo.jp/entry/2018/12/29/104132

### ビルドオプションが認識されずコンパイルエラー扱いになってしまう問題

上記を解消したら、 今度は以下のコンパイルエラーが発生しインテリセンスが効かなくなってしまいました。

```
Unknown argument: '-fno-lifetime-dse'clang(drv_unknown_argument)
```

clangdでサポートされていないコンパイルフラグを指定してしまったのが原因のため、 `.clangd` で無視する設定を入れることで解消しました。
**全てのサブディレクトリに配置する必要がある** 点に注意です。

```yaml:.clangd
# use for syntax highlight
CompileFlags:
  Remove:
    - -fno-lifetime-dse
```

詳細はこちらのissueをご覧ください。

https://github.com/clangd/clangd/issues/1142#issuecomment-1790904596

これで、インテリセンスが効くようになり、ローカル環境の開発準備も無事整いました。

# おわりに

以上、MLIR dialectをイメージで配布する方法の紹介でした。イメージが重いことを除けば、比較的手軽にコンパイラを使い始められるようになったと思います。マルチステージビルドでdialectのバイナリとLLVM本体だけにすることで軽量化もできるかもしれません。

自分の開発環境も整ったところで、次はいよいよコンパイラを実装していきたいと思います。

[^llvm-17]: [Learn LLVM 17](https://www.amazon.co.jp/Learn-LLVM-17-beginners-libraries/dp/1837631344) を写経していたので、LLVM 17だけ事前にインストールされています。ちなみにこのときもソースコードからビルドしました。
[^project]: mlir-helloではthird-party配下にllvm-projectが[まるごと格納](https://github.com/Lewuathe/mlir-hello/tree/main/thirdparty/llvm-project)されており、それをビルドして使用しています。