Timee Product Team Blog

タイミー開発者ブログ

Vertex AI Pipelinesを効率的に開発するための取り組み(part2)

こんにちは、タイミーのデータエンジニアリング部 データサイエンス(以下DS)グループ所属のYukitomoです。

DSグループではMLパイプラインとしてVertex AI Pipelinesを利用しており、その開発環境の継続的な効率化を進めていますが、今回はここ最近の変更点を紹介したいと思います!

Vertex AI PipelinesについてはGoogle Cloudの公式ページや、前回の記事を参照ください。

モノレポ環境に移行

当初はパイプライン毎にレポジトリを用意していました。しかしながら新規でパイプラインを起こす度にレポジトリの作成から行うのは、

  • ちょっとした“作業”ではあるのですが気軽に行うには少し腰が重い
  • cookie cutter を使って初期状態を揃えたり前回の記事のような標準化を行っても、異なるレポジトリを異なる開発者によりメンテナンスを行うと、それぞれが独自の進化をしたり属人化したりしがち

といった問題を抱えていました。そこで、モノレポ化して1箇所に集積し、

  • CI/CDや、パイプラインビルド等の付随スクリプトを共用化、付随スクリプトのような非本質的な機能開発を効率化
  • フォルダ構成やファイルの命名規則などを統一、“隣のプロジェクト”を参照しやすくすることで、ベストプラクティスを共有化、パイプラインの機能そのものの開発を効率化

することを目指しました。同一レポジトリ傘下に収めることで従来よりも敷居が下がり、共通知化を進められていると感じています。

コンテナイメージの共通化

パイプラインを1箇所のレポジトリに集めた段階でパイプラインのコンポーネントは200個以上あり、パイプラインのコンポーネントそれぞれが独自にDockerfileやpyproject.tomlを持っていました。脆弱性対応や機能追加のための依存モジュールのアップデートはそれぞれのpyproject.tomlを更新することになりますが、ファイルの数が多いと更新に手間がかかってしまいます。そこで、同一パイプラインのコンポーネント間ではコンテナイメージを共用できるような形にアーキテクチャを改めました。

おおまかな方針は以下のとおりです。

  1. コンポーネントの入出力を定義するyamlファイル(component.yaml)はそれぞれ名前を変え、1つのフォルダにまとめる。
  2. コンポーネントの中のロジックを記述するpython コードも1箇所にまとめ、全体をコンテナイメージに複製。
  3. 単一コンテナだけでは処理しきれない場合を考慮し、複数のコンテナイメージを格納できるようコンテナイメージ用のフォルダは階層化。

従来のアーキテクチャ

% tree PIPELINE_X

./PIPELINE_X
├── components
│   ├── component_A         # コンポーネント毎にフォルダを用意し、
│   │   ├── Dockerfile      # Dockerfile/pyproject.tomlはそれぞれ独立に配置
│   │   ├── component.yaml
│   │   ├── pyproject.toml
│   │   └── src
│   │       └── ...
│   ├── component_B
│   │   ├── Dockerfile
│   │   ├── component.yaml
│   │   ├── pyproject.toml
│   │   ├── src
│   │       └── ...
│   └── component_C
│       ├── Dockerfile
│       ├── component.yaml
│       ├── pyproject.toml
│       ├── src
│           └── ...
└── pipelines
    └── main.py 

新しいアーキテクチャ

% tree ./PIPELINE_X

./PIPELINE_X
├── components
│   ├── definitions           # 1. component.yamlは1箇所に集約
│   │   ├── component_a.yaml
│   │   ├── component_b.yaml
│   │   └── component_c.yaml
│   └── src                   # 2. src以下全てをコンテナイメージに複製.
│       ├── component_a.py    # component_*.yamlの設定で動作するpython fileを指定
│       ├── component_b.py    # DockerfileのCMDを指定するイメージ
│       └── component_c.py
│
├── containers                # 3. 単一コンテナでは難しい場合に備え
│   └── main                  # 複数のコンテナを利用できるようフォルダを階層化
│       ├── Dockerfile
│       └── pyproject.toml
└── pipelines
    └── main.py

コンテナ数を減らすことで、dependabotの運用は格段に楽になりました。コンテナが減ることで警告の数も下がり、警告の数が下がることで更新の初動も取りやすくなるという好循環のおかげで、2024年10月現在、dependapotからの警告は画面のスクロールが不要な範囲にはおさまるようになってきました。

また、コンテナを集約する段階で気がついたのですが、いくつかのDockerfileの中で利用するpoetryのバージョンが古いままだったり、マルチステージのビルドが正しく行われていなかったりするものも少なからずありました。Dockerfileに限らずコードライティング全般に言えることですが、記述量は可能な限り少なくする方が、このような小さな不具合の発生は抑制でき、安定したコードを供給できます。

CDにおけるビルドキャッシュの利用

タイミーDSグループにおいて、CI/CD環境はGitHub Actions、クラウド環境はGoogle Cloudを利用しています。指定されたトリガー条件が発生した時にコンテナイメージをビルドするのですが、GitHub Actionsの場合、ジョブ単位でVMが異なるため、連続するGitHub Actionsの実行の場合でもdockerのビルドは一からやり直しになってしまいます。 そこで、こちらこちらの内容を参考に、ビルド結果をVMの外部、Artifact Registryにキャッシュ、次回実行時に再利用することでCI/CD の処理を高速化させました。なお、以下の設定ではbuildcacheは通常のコンテナ用Artifact Registry(下のコードで言うと${IMAGE_NAME})とは異なるRegistry( ${IMAGE_NAME}-buildcache )に保存しています。

GitHub Actions内での記述より抜粋

docker buildx create --name container --driver=docker-container

docker buildx build \
  --builder=container \
  :
  --cache-from=type=registry,ref=${IMAGE_NAME}-buildcache \
  --cache-to=type=registry,ref=${IMAGE_NAME}-buildcache,mode=max \
  -t ${IMAGE_NAME}:${IMAGE_TAG} \
  --load .
              
docker push ${IMAGE_NAME}:${IMAGE_TAG}

# -cache-from/-cache-toに指定するrefの値にsuffix '-buildcache'を付加し
# 本来のイメージとキャッシュイメージの置き場所を分離しています。

パイプライン命名規則の工夫

MLパイプラインを開発していると、

  • あるパイプラインを少しだけ変えたパイプラインを実現したい
  • Gitの別ブランチで管理すればいいんだけれど、比較しながらコード書きたい

といったケースはよくあると思います。簡単にこれを実現しようと cp -r でパイプラインを丸ごとコピーしたとしても、従来のアーキテクチャでは様々な設定(パイプラインの名前、参照するコンテナイメージの名前、バージョン情報)を書き換える必要があります。そのため、煩わしい作業が発生していました。また、それらの設定方法も統一が取れておらず、“微妙に”パイプライン毎に異なっていました。そこで、それらのバージョン情報以外の情報を全てパイプラインが保存されているフォルダのパス情報から取得するよう統一し、cp -r だけですぐにパイプラインの亜種が作成できるようにしました。

従来のアーキテクチャ

# 1. パイプラインの名前はパイプラインのpyproject.toml内のname属性や環境変数(dotenv)を利用
# 2. コンテナイメージの名前は コンテナイメージのpyproject.toml内のname属性を利用
# 3. Version情報:
#    パイプライン    -> パイプラインのpyproject.toml内のversionを利用
#    コンテナイメージ -> コンテナイメージのpyproject.toml内のversionを利用
 
. PIPELINE_X
├── components
:
│
├── containers
│   └── main
│       ├── Dockerfile
│       └── pyproject.toml    # 2 [tool.poetry].name    -> パイプラインの名前# 3 [tool.poetry].version -> パイプラインのversion
├── pipelines
│   └── main.py
├── .env.* (prod/stg/dev..)   # 1 パイプラインの名前は .envからやpyproject.tomlなど
└── pyproject.toml            #   各種のやり方が存在。
                              # 3 [tool.poetry].version -> コンテナイメージのversion

# Compile されたpipeline config抜粋
{
  :
  "deploymentSpec": {
    "executors": {
      "exec-comp_a": {
        "container": {
        :
          "image": "${GAR_REGISTRY_PREFIX}/blahblahblah:vX.Y.Z"
          # 2. コンテナイメージの名前"blahblahblah"はコンテナイメージのpyproject.tomlより
          # 3. コンテナイメージのVersion "vX.Y.Z"はコンテナイメージのpyproject.tomlより
        }..}..}..},
      :
  "pipelineInfo": {
    "name": "arbitrary_string"
    # 1. パイプラインの名前は
  },

# 注: ${GAR_REGISTRY_PREFIX} は Artifact Registry のアドレス

新しいアーキテクチャ

# 1. パイプラインの名前はRepository内のフォルダ位置を利用 (= ${SERVICE}-${PIPELINE} )
# 2. コンテナイメージの名前は "${パイプラインの名前}"-"${コンテナのフォルダ名}"
# 3. Version情報:
#    パイプライン    -> パイプラインのpyproject.toml内のversionを利用
#    コンテナイメージ -> パイプラインのversionを利用

SERVICE/**/vertex_ai_pipelines/PIPELINE_X  # 1 
├── components
:
│
├── containers
│   └── main                             # 2
│       ├── Dockerfile
│       └── pyproject.toml
├── pipelines
│   └── main.py
├── pyproject.toml
└── .env.* (prod/stg/dev..) 

# Compile されたpipeline configの一部を抜粋
{
  :
  "deploymentSpec": {
    "executors": {
      "exec-comp_a": {
        "container": {
        :
          "image": "${GAR_REGISTRY_PREFIX}/${SERVICE}-${PIPELINE_X}-main:vX.Y.Z"
        }..}..}..},
      :
  "pipelineInfo": {
    "name": "${SERVICE}-${PIPELINE_X}"
  },
  :
  
# 注: ${GAR_REGISTRY_PREFIX} は Artifact Registry のアドレス

ちょっとした変更ではあるのですが、新しいパイプラインを構築する際の初動を早くすること、また簡単にできることにより、新しい方式を試そうという心理的な敷居を下げることができていると考えています。

今回紹介した取り組み以外にも、Vertex AI Pipelinesに限らず効率化するための具体的なアイデアはいくつかあるのですが、プロダクションを動かしながら変更しており、障害の発生を抑えるためにも、一度に大きな変更は与えずステップを踏みながらMLOps基盤を理想の姿に近づける活動を続けています。

We’re Hiring!


タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!!

現在募集中のポジションはこちらです!

「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!