サーバレスGPUにModalがいいぞ!
GPUを使いたいこと、あると思います。
ただしご家庭に強いGPUはないこともあるでしょうし、かといってGPU搭載したインスタンスを立て続けているととてつもないお金がかかります。
そんなあなたにサーバレスGPU、使った秒数分だけ課金が発生するので、いきなりすごい金額がかかることにはならず手軽にGPUを使ったプログラミングが始められます。
私は今の所ModalとRunPodを試したのですが、ModalのDXが良すぎるため今回イントロ記事を書く筆を取りました。
始めにざっくりいいところを述べると
- 一つのファイルで完結する。
- RunPodとの比較だと「RunPodではDockerのイメージを自前でビルドしてどこかしらのregistryにpush、都度タグを付け替えてRunPodの方にも反映」みたいなのが必要だったがModalでは全部要らない
- ローカルパッケージをちょろっとした関数で送れたり、テキスト以外のPythonオブジェクト実行時の引数に詰められて細かいところが色々便利
などなどです。
ちなみにお値段なのですが、他のサーバレスGPUサービスと比べると高めなのですが、どのくらい使うかにも寄りますがそれを補ってあまりあるDXの高さがあると感じています。
A100で1時間という条件で比較してみますと次のような料金です。(2024年10月確認時点での料金です)
- Lambda Labs(40GB): $1.29
- Cudo Compute(80GB): $1.59
- RunPod(80GB): $2.17
- Modal(40GB): $3.17
- Modal(80GB): $4.75
他のサービスとの比較については下記の記事も参考にしてみてください。
Getting Started
ModalのGetting Startedをなぞるだけではあるのでサクッと行きます。
まずはSign Upアカウントを作りましょう。
pip install modal
でインストールします。これでCLIから modal
コマンドが使えるようになります。
modal setup
を実行するとブラウザから認証ができるので、アカウントを選ぶとローカルにトークンが保存され、modal上の処理を実行できるようになります。
ちなみにこれだと手動で認証した自分の手元からしか実行できないので、実際にプロダクトで動かす時には次のような環境変数を仕込みます。
MODAL_TOKEN_ID=
MODAL_TOKEN_SECRET=
中身の値は https://modal.com/settings/{user_name}/tokens からNew Token
をクリックすることで取得できます。
ではとりあえずHello Worldしてみましょう!
まずはスクリプトを用意します。
import sys
import modal
# Modalで事前に作る必要などはなくこちらで自由に名前をつけられます
# 名前を変えると別アプリと判定され、ログも分かれますし、後述するDockerイメージのビルドも再起動されます
app = modal.App("example-hello-world")
# こちらがModal上で実行されます
@app.function()
def f(i):
if i % 2 == 0:
print("hello", i)
else:
print("world", i, file=sys.stderr)
return i * i
# こちらは自分の実行環境なので、ローカルのパッケージなりファイルなりにアクセスできます
@app.local_entrypoint()
def main():
# run the function locally
print(f.local(1000))
# run the function remotely on Modal
print(f.remote(1000))
# run the function in parallel and remotely on Modal
total = 0
for ret in f.map(range(200)):
total += ret
print(total)
そして実行します。
modal run test.py
するとこんな感じでログが表示されて実行されているのた見て取れます。
✓ Initialized. View run at https://modal.com/apps/kazuyaseki/main/ap-xxxxxxxxxxxxxxxxxx
✓ Created objects.
├── 🔨 Created mount /modal/test.py
└── 🔨 Created function f.
hello 1000
@functionのパラメータ
というのが至極シンプルな例ですが、このままだとpythonの諸々便利なパッケージが参照できなかったり、他にも必要なファイルを送りたかったりするかもしれません。なので必要なパラメータを一通り紹介していきます。
一通りのパラメータはドキュメントのこちらに全部載ってますが、よく使うものだけ抜粋していきます。
gpu
やはりサーバレスGPUを使うのでどのGPUを使うかは非常に重要でしょう。
現状では次の種類のGPUたちがサポートされています。
- “t4” → GPU(T4, count=1)
- “l4” → GPU(L4, count=1)
- “a100” → GPU(A100-40GB, count=1)
- “h100” → GPU(H100, count=1)
- “a10g” → GPU(A10G, count=1)
こんな感じでfunctionの引数として渡します。
@app.function(
gpu="A10G"
)
もちろんGPUによって費用は変わります。(2024年10月時点での料金)
GPU モデル | 価格(/秒) | 価格(/時間) |
---|---|---|
Nvidia H100 | $0.001644 | $5.92 |
Nvidia A100, 80 GB | $0.001319 | $4.75 |
Nvidia A100, 40 GB | $0.000881 | $3.17 |
Nvidia A10G | $0.000306 | $1.10 |
Nvidia L4 | $0.000222 | $0.80 |
Nvidia T4 | $0.000164 | $0.59 |
image
Modalでは環境を整えていくにあたってDockerを使います。
-
.from_registry
でDocker Hubに公開されてるimageを指定できます -
apt_install
でシステムパッケージをインストールできます -
pip_install
でPythonパッケージをインストールできます。バージョンを指定しない場合最新のものがインストールされます。
import modal
@app.function(
image=modal.Image.from_registry("python:3.11-slim")
.apt_install("ffmpeg")
.pip_install(
"ffmpeg-python==0.2.0",
"pillow==10.4.0",
),
gpu="A10G",
timeout=7200,
)
def my_function():
# 何かやる
こう書いて実行するだけでビルドしてくれます。最高だ...
Building image im-xxxxxxxxxxxxxx
=> Step 0: FROM base
=> Step 1: RUN python -m pip install ffmpeg-python==0.2.0 pillow==10.4.0
Looking in indexes: http://pypi-mirror.modal.local:5555/simple
Collecting ffmpeg-python==0.2.0
Downloading http://pypi-mirror.modal.local:5555/simple/ffmpeg-python/ffmpeg_python-0.2.0-py3-none-any.whl.metadata (1.7 kB)
Collecting pillow==10.4.0
Downloading http://pypi-mirror.modal.local:5555/simple/pillow/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (9.2 kB)
Collecting future (from ffmpeg-python==0.2.0)
Downloading http://pypi-mirror.modal.local:5555/simple/future/future-1.0.0-py3-none-any.whl.metadata (4.0 kB)
Downloading http://pypi-mirror.modal.local:5555/simple/ffmpeg-python/ffmpeg_python-0.2.0-py3-none-any.whl (25 kB)
Downloading http://pypi-mirror.modal.local:5555/simple/pillow/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl (4.5 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 671.2 MB/s eta 0:00:00
Downloading http://pypi-mirror.modal.local:5555/simple/future/future-1.0.0-py3-none-any.whl (491 kB)
Installing collected packages: pillow, future, ffmpeg-python
Successfully installed ffmpeg-python-0.2.0 future-1.0.0 pillow-10.4.0
Saving image...
Image saved, took 881.08ms
Built image im-xxxxxxxxxxxxxx in 4.83s
✓ Created objects.
├── 🔨 Created mount /modal/test.py
└── 🔨 Created function f.
また、内容が変わった時でも再度実行した時に再ビルドしてくれます。
注意点としてはビルドには当然のことながらそれなりに時間がかかるので、ユーザに提供するところで使う時は何らか先んじでビルド実行しておくことです。
CUDA入れる
CUDA入ってるイメージ引っ張ってくるのが楽です。add_python
を足すことでPythonを入れられます。
@app.function(
image=modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
)
カスタムDockerfile読む
他にも色々カスタマイズしたいので最早普通にDockerfileを書きたいこともあるかもしれません。その場合は次のようにDockerfileへのパスを通すだけでOKです。
@app.function(
image=modal.Image.from_dockerfile("Dockerfile_custom", add_python="3.11")
)
ローカルのデータを渡す
まず一つに、シンプルに実行元環境からリモート関数を呼び出す時の引数に入れるのが一つの方法です。
地味に聞こえるかもしれませんがこれがめちゃくちゃ便利です。他のサービスでは基本文字列として渡してます。
import json
@app.function()
def foo(a):
print(sum(a["numbers"]))
@app.local_entrypoint()
def main():
data_structure = json.load(open("blob.json"))
foo.remote(data_structure)
ドキュメントには次のように書いており、
Any data of reasonable size that is serializable through cloudpickle is passable as an argument to Modal functions.
こちらのライブラリでシリアライズできるものであれば何でも渡せるみたいです。音声や動画などのバイナリまで渡せるかは分からないですが、Pythonオブジェクトなら基本なんでも渡せそうです。
次にmounts
を使って渡す方法です。
実行元の環境から必要なファイルを渡すことができるようになります。
ディレクトリ毎送る方法とファイル単体で送ることができます。remote_pathがModal実行環境内でのパスなので、送ったファイルを使う時はこちらで指定したパスを参照します。
@app.function(mounts=[
# ディレクトリ毎渡す
modal.Mount.from_local_dir("/hoge/fuga", remote_path="/root/"),
# ファイルを渡す
modal.Mount.from_local_file(
local_path="~/hoge.yml",
remote_path="/root/hoge.yml",
)
])
def some_func():
# 何かやる
そして結構大事なのがローカルのPythonパッケージを渡す方法です。
手元で開発しているものを後々Modalに処理を移行したいとなった時に、逐一コピペは存外面倒なので、こうやってパッケージを気軽に移せるのは大変重宝します。
import modal
import my_local_module
app = modal.App()
@app.function(mounts=[modal.Mount.from_local_python_packages("my_local_module", "my_other_module")])
def f():
my_local_module.do_stuff()
secrets
シークレットを渡すにあたっては二つ方法があり、一つはModalに登録しておく方法と、もう一つは実行元の環境から渡す方法です。
Modalをブラウザから開くと Secrets
タブがあるのでそちらを開いて新しいシークレットを追加します。
実行時に登録した名前を使うことでシークレットを os.environ に仕込むことができます。
@app.function(secrets=[modal.Secret.from_name("my-custom-secret")])
def some_function():
secret_key = os.environ["KEY_NAME"]
実行時に追加することもできます。
import os
import modal
app = modal.App()
local_secret = modal.Secret.from_dict({"FOO": os.environ["LOCAL_FOO"]})
@app.function(secrets=[local_secret])
def some_function():
print(os.environ["FOO"])
また、.env
があれば .Secret.from_dotenv()
を使うと読み込むことができます。
@app.function(secrets=[modal.Secret.from_dotenv()])
def some_other_function():
print(os.environ["USERNAME"])
まとめ
以上、Modalの基本的な使い方の紹介でした。
かなり簡単に使い始められるのでオススメのサーバレスGPUです。
皆さんもぜひ使ってみてください!
Discussion