💭

Pythonの最適化ライブラリPuLP をM1 Mac × Dockerで動かす

2023/10/01に公開

目的

M1 Mac上のDockerコンテナ内で、Pythonの数理最適化ライブラリPuLPを動かしたい。

事象

  • M1 Mac上のDockerコンテナに、単にPuLPをインストールしてoptimizeメソッドを実行すると、以下のようなエラーが生じる。
    OSError: [Errno 8] Exec format error: '/src/.venv/lib/python3.11/site-packages/pulp/solverdir/cbc/linux/64/cbc'
    
  • Pythonの仮想環境venvでは、上記エラーは発生しなかった。

推定原因

  • 上記エラーは、最適化ソルバーであるcbcがARMアーキテクチャに対応していないことを意味している。[1]
  • 上記エラーコードにあるソルバーフォルダsolverdirには、linuxの他にもosxがあり、Mac os環境下(今回のvenv)ではおそらくそちらが実行されている。つまり、プログラム実行環境のos違いによって参照するソルバーのパスが異なると、推定される。
  • Mac osの場合のソルバーはおそらくM1チップ環境下でも実行できるものであり、Linuxの場合のソルバーはM1チップに対応していないと考えられる。したがって、venv環境ではM1チップで実行可能なソルバーが実行され、Docker環境下ではM1チップで実行不可能なソルバーが実行されていると、推定される。

対応策

  • Linux及びarm64環境下で実行可能な、cbcバイナリファイルを別でインストールし、PuLP実行時にソルバパスを指定することで、上記エラーは解消された。
  • 具体的な手順は以下の通り。
    • Dockerfileに以下の行を追加する。以下はLinux向けのcbcソルバをインストールするコマンドである。[2][3]コンテナ内では/usr/bin/cbcに配置される。
      RUN apt-get update && apt-get install -y coinor-cbc
      
    • PythonスクリプトでPuLPを使用する際は、以下のようにソルバパスを設定する。[3:1]ただし、problemは最適化問題が定義されているオブジェクト。
      path_to_solver = '/usr/bin/cbc'
      solver = pulp.COIN_CMD(path=path_to_solver)
      problem.solve(solver)
      

サンプルコード

環境構築用コード

Dockerfile
#python3.11のイメージをダウンロード
FROM python:3.11-buster

WORKDIR /src

# Linux向けのcbcソルバをインストール
RUN apt-get update && apt-get install -y coinor-cbc

COPY requirements.txt /tmp/requirements.txt
RUN python3 -m pip install -r /tmp/requirements.txt
docker-compose.yaml
version: '3'
services:
  demo-app:
    build: .
    volumes:
      - .:/src
    tty: true #docker compose up時にコンテナを起動し続ける設定。
requirements.txt
pulp==2.7.0

上記Dockerfile, docker-compose.yaml, requirements.txtを用いて構築したコンテナ内で、以下のmain.pyが無事実行できれば良い。なお、main.pyは以下の最適化問題を解くためのプログラムである。

\begin{align} \rm{max}\ &x_1 + 2x_2 +3x_3\\ \rm{subject\ to}\ &4x_1 + 2x_2 +x_3\leq 3\\ &x_1 - 2x_2 +x_3\leq 2\\ &x_i \in \{0, 1\}\ (i=1,2,3) \end{align}
main.py
import pulp

# 問題を定義する。
problem = pulp.LpProblem( "demo", pulp.LpMaximize )

# 変数を定義する。
x = [ pulp.LpVariable( f"x_{i}", cat=pulp.LpBinary ) for i in range(3) ]

# 係数を設定する。
c = [ 1, 2, 3 ]
a1 = [ 4, 2, 1 ]
b1 = 3

a2 = [ 1, -2, 1 ]
b2 = 2

# 目的関数を設定する。
problem += pulp.lpDot( c, x )

# 制約条件を設定する。
problem += pulp.lpDot( a1, x ) <= b1
problem += pulp.lpDot( a2, x ) <= b2

# cbcソルバーのパスを設定する。
path_to_solver = '/usr/bin/cbc'
solver = pulp.COIN_CMD(path=path_to_solver)

# 最適化を実行する。
result = problem.solve(solver)

# 結果を出力する。
print(f"目的関数の値: {pulp.value(problem.objective)}")
print("最適解")
for i in range(len(x)):
    print(f"x_{i} = {pulp.value(x[i])}")

実行結果は以下の通り。

目的関数の値: 5.0
最適解:
x_0 = 0.0
x_1 = 1.0
x_2 = 1.0
脚注
  1. PuLPのgithubのissue(https://github.com/coin-or/pulp/issues/433) ↩︎

  2. ubuntuへのcbcソルバインストール(https://howtoinstall.co/package/coinor-cbc) ↩︎

  3. 同様の事象に関するgithubのissue(https://github.com/davidusb-geek/emhass-add-on/issues/9) ↩︎ ↩︎

Discussion