Jijのプロダクト開発にRustを採用した
この記事はJij Inc. Advent Calendar 2023の8日目の記事です。
こんにちは、株式会社Jijのしたろうです。
Rustを使ってJijZeptのバックエンドを開発するチームをマネジメントしています。
なぜ私はRustを採用したのか?
私がJijに入社して以来、Rustを用いて2つのプロダクトを実装しました。一つ目は、数理モデルを記述するPythonパッケージ jijmodeling
、二つ目は最適化問題を解くクラウドサービス JijZept のバックエンドです。
入社初期に、私はJijが提供するプロダクトの実装言語を選定する役割を担いました。我々のプロダクトは、より高速に問題を解くことが要求されます。この要件を満たすため、C++とRustを候補に挙げました。また、顧客の使用言語はPythonであると設定していたため、Python向けのバインディングが必須でした。C++には pybind11 と Boost.Python、Rustには PyO3 があり、いずれの言語もこの要件を満たしていました。
当時、私はRustよりもC++の方が経験があったので、C++を選択することも考えてました。実際、在学中はC++とCUDAを使って博士課程卒業まで数値計算をしてましたし、Jijが創業して間もない頃には、C++のテンプレートメタプログラミングをOpenJijに導入した経験(コミットログ)もありました。
しかし、C++を使うことはcmakeと向き合うことであり、それはとてもとてもしんどいことです。OpenJijを書き直していた時は、EigenやCUDAといった複数のライブラリの依存関係を解決するのに多大な時間を費やしました。cmakeはパッケージマネージャではなく、C++にはパッケージ配布のための形式やレジストリもなく、C++で資産を利用しようとすると途端にコストが跳ね上がります。
おそらくC++を使うためにcmakeを書いたことのある人がcargoを使ってみると、それだけでRustを使いたくなるのではないかなと思います。
また、作りたいプロダクトはいわゆる数理モデルのコンパイラであり、パターンマッチを使うシーンが多いことが予想されました。Rustはパターンマッチを文法レベルでサポートしているので、この点でもうれしいです。
最終的に、モダンな文法をサポートしていて、パッケージ管理がしやすく、高速に動作できるRustを選択するのが、これから技術ストックを積み上げていくJijの将来にとって、最善の選択になると確信しました。以上が、私がJijにRustを採用した理由です。
JijModelingの紹介
2023年8月7日に jijmodeling v1.0.0 をリリースしました。それ以前のバージョンはPythonで実装されていましたが、v1.0.0からは完全なRust実装になりました。ここでは、JijModelingを簡単に紹介します。
JijModeling はモデラーです。様々なソルバーに最適化問題を求解させるための共通のインターフェースを提供します。ソルバーではないので、JijModeling単体では最適化問題を解くことはできません。JijModelingTranspiler や弊社が提供するJijZeptと併用することで、最適化問題を解くことができます。
数理最適化のためのソルバーやモデラーはオープンソースから商用まで様々なものが存在します。その中で、他のモデラーとは大きく異なる、JijModelingの最大の特徴は「モデルが先、データは後」という考え方です。
例えば、バイナリ変数
一般的なモデラーでは、事前に与えるべきデータをモデルの記述に使います。例えば、
N = 10 # 実データを使う
x = BinaryVar("x") # バイナリ変数の定義
sum = 0
for i in range(N): # 実データをモデルと混ぜている
sum = sum + x[i]
一方、JijModelingは「モデルが先、データは後」という考え方なので、実データそのものではなく、それを入力する対象を使います。JijModelingでは、その対象を Placeholder と呼びます。今ここで考えている例では
import jijmodeling as jm
N = jm.Placeholder("N") # 実データを入力する対象を表す
i = jm.Element("i", belong_to=(0, N)) # 添字iは[0,N)を走る
x = jm.BinaryVar("x", shape=(N,)) # バイナリ変数の定義
sum = jm.sum(i, x[i])
JijModelingの「モデルが先、データは後」というアプローチには、いくつかの重要な利点があります。このアプローチのもと、モデルとデータが分離されるため、同一のモデルを異なるデータセットで利用できるようになります。これは、特に異なるケースを比較分析する際に大きな利益をもたらします。
例えば、Traveling Salesman Problem(TSP)では、都市間の距離や都市数が変わるケースに対しても、同一のモデルを再利用できます。TSPのモデルを考えるとき、都市間の距離や都市数 (N) が変わっても、以下のようなモデルは変わりません:
# 仮の都市数Nと都市間距離matrixを設定
N = jm.Placeholder("N")
matrix = jm.Placeholder("matrix", shape=(N, N))
# 決定変数の定義
x = jm.BinaryVar("x", shape=(N, N))
# TSPの目的関数と制約条件の定義
# ここでは詳細なコードは省略
同様に、混合整数計画問題 (MIP: Mixed Integer Problem)
で記述できる最適化問題は多いですが、その表現力の源泉は Placeholder
ちなみに、JijModeling は jijmodeling.dataset
サブモジュールを通じて、MPSフォーマットのデータを読み込む機能を提供しています。また、それを利用してMIPLIBが提供するMPSファイルを読み込むローダーも提供しています。これにより、ユーザーはMIPモデルを簡単に使うことができます。例えば “neos5” というMIPモデルをソルバーに解かせたい場合は、
import jijmodeling as jm
# MIPLIBからMPSファイルをローカルにダウンロードする。
miplib = jm.dataset.Miplib()
# キャッシュしたMPSファイルのうち "neos5.mps.gz" を読み込む。
# problem: JijModelingのProblemクラスで記述されたMIPモデル
# instance_data: problemに含まれるPlaceholderに入力するデータ
problem, instance_data = Mpilib.load("neos5")
# 以下では problemとinstance_dataをソルバーに入力して解く
# ...
と実行するだけでモデルとデータの準備が完了できます!
より詳細なJijModelingの説明や使い方については、下記リンクのドキュメントを参考にしてください。
最後に
\Rustエンジニア・数理最適化エンジニア募集中!/
株式会社Jijでは、数学や物理学のバックグラウンドを活かし、量子計算と数理最適化のフロンティアで活躍するRustエンジニア、数理最適化エンジニアを募集しています!
詳細は下記のリンクからご覧ください。皆さんのご応募をお待ちしております!
Rustエンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/51062
数理最適化エンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/75132
Discussion