WDLをGridEngineで使う

ワークフロー言語である[WDL](http://www.openwdl.org/)を[Cromwell](https://software.broadinstitute.org/wdl/)を使ってGridEngineと連携させて動かす方法を紹介します。一応GridEngineを例に説明しますが、[Torque](http://www.adaptivecomputing.com/products/torque/)や[Slurm](https://slurm.schedmd.com/)のようにディスクを計算ノード間で共有してジョブスケジューリングしてくれるものであれば多少の改変で対応可能なはずです。日本のアカデミックの共有計算機でよくある構成で利用可能なはずです。

WDLとは
=======
[WDL](http://www.openwdl.org/)とはBroad Instituteが中心になって開発するワークフロー言語です。[CWL](https://www.commonwl.org/)や[Snakemake](https://snakemake.readthedocs.io/en/stable/)などワークフロー言語は[星の数ほど](https://github.com/common-workflow-language/common-workflow-language/wiki/Existing-Workflow-systems)存在しますが、WDLはゲノム解析の定番ソフトウェアである[GATK](https://software.broadinstitute.org/gatk/)のお膝元で作られていることもあり、GATKでどのように解析すればよいかはWDLとしてワークフローが提供されています。また、[gnomAD](https://gnomad.broadinstitute.org/)のような大規模解析でも実績があるため、万が一数千、数万のゲノム解析をやらざるをえなくてなっても対応可能です。

Cromwellとは
===========

[Cromwell](https://software.broadinstitute.org/wdl/)はWDLの実行エンジンです。Cromwell自身はWDLとCWLに対応しており、各種クラウドプラットフォームやジョブスケジューラに対応しています。Scalaで書かれており、jarファイルひとつで完結しているため、Javaのランタイムさえあれば依存関係に悩むことなくセットアップすることができます。

準備するもの
===========
* GridEngine ([UGE](http://www.univa.com/products/), [SGE](https://github.com/son-of-gridengine/sge)など)
    * Cent OS 7でSon of GridEngineをビルドする方法は[gist](https://gist.github.com/informationsea/d82c4cb2f98a6ccfc2958c219203b0a6)にあるDockerfileをご覧ください
    * Son of GridEngineのセットアップ方法は[私の昔のQiitaの記事](https://qiita.com/informationsea/items/d9c61186a2d9434b4c23)をご覧ください
* [Singularity](https://github.com/sylabs/singularity)
    * [Docker](https://www.docker.com/)でも構いませんが、Dockerは通常はRoot権限が必要なため共有計算機環境では使いづらいはずです
    * [Podman](https://podman.io/)という選択肢もありますが、連携させたことはありません
* [カスタム版Cromwell](https://github.com/informationsea/cromwell/releases)
    * 若干使いやすくするためにカスタムをしています
    * 詳細は設定ファイルの中で説明します

設定ファイル
==========

Cromwellの設定は設定例が[CromwellのGitHub](https://github.com/broadinstitute/cromwell/tree/develop/cromwell.example.backends)に載っています。ただ、ここに記載の例では不足する項目も多いので以下に私が使っているものを紹介します。

```conf
include required(classpath("application"))

# サーバーの待ち受けポートとIPアドレスの設定
webservice {
  port = 8080
  interface = 0.0.0.0
}

# Call Cacheを有効化し、同じデータに対して同じ処理をするときには以前使ったデータを再利用する
call-caching {
  enabled = true
}

# ファイルのハッシュをキャッシュしておく
system {
  file-hash-cache = true
}

# Docker Hubへ接続しない
# インターネットから切断した環境では必要
services {
  HealthMonitor {
    config {
      check-dockerhub = false
    }
  }
}

# データベースに接続する
# しなくても使えないことはないがcall cacheを使うためには必須
# H2の組み込みデータベースも使えるが、安全のためにも外部のDBを使う方がよい
database {
  profile = "slick.jdbc.MySQLProfile$"
  db {
    driver = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://mysqlserver/cromwell?rewriteBatchedStatements=true&useSSL=false"
    user = "cromwell"
    password = "password"
    connectionTimeout = 5000
  }
}

# DockerのイメージのハッシュをDocker Hubと直接通信するのではなくdockerコマンドの呼び出しで行う
# インターネットから切断されている環境では必須
# 実際にはSingularityを使うためにdockerコマンドを模したシェルスクリプトでごまかす
docker {
  hash-lookup {
    method = "local"
  }
}


# 実行エンジンの設定
backend {
  # SGEと名前がついているが、下の providers の中に書く名前と一致させればよい
  default = "SGE"

  providers {
    SGE {
      # Shared file systemのバックエンドを利用する
      actor-factory = "cromwell.backend.impl.sfs.config.ConfigBackendLifecycleActorFactory"
      config {
        filesystems {
          local {
            localization: [
              "soft-link"
            ]
            caching {
              # あるジョブの計算結果を別のジョブの入力とするときにどのようにするかを指定
              # hard-link, soft-link, copy, cached-copy が利用可能
              # オフィシャルのcromwellはdockerを使うジョブでsoft-linkが使えないが、カスタム版ではその制限を外してある
              duplication-strategy: [
                "soft-link"
              ]
              # ファイルの同一性判定をどのようにやるかを指定
              # デフォルトはfileだが、入力ファイルが多いとハッシュでCPUを食いつぶすのでパスと変更日時から作ったハッシュで代用する
              # この場合はsoft-linkのみしか使えない
              hashing-strategy = "path+modtime"
            }
          }
        }

        # 実行時パラメータとして受け入れるものを指定
        # CPUコア数と利用メモリ、docker imageが指定でき、docker imageはオプション扱い
        runtime-attributes = """
          Int cpu = 1
          Float memory_gb = 4
          String? docker
        """

        # Dockerを使わない場合のジョブの投入
        # 最初にスロットあたりのメモリ使用量を計算し、それを元にmem_reqとs_vmemの２つのパラメータを設定している
        submit = """
          MEM_G_PER_CORE=`echo 'scale=3; ${memory_gb}/${cpu}'|bc`G

          qsub \
          -terse \
          -V \
          -b y \
          -N ${job_name} \
          -wd ${cwd} \
          -o ${out} \
          -e ${err} \
          -pe def_slot ${cpu} \
          -v JAVA_TOOL_OPTIONS="-XX:+UseSerialGC -XX:CICompilerCount=2" \
          -l mem_req=$MEM_G_PER_CORE,s_vmem=$MEM_G_PER_CORE \
          /usr/bin/env bash ${script}
        """
        # Dockerを使う場合のジョブの投入
        # 最初にスロットあたりのメモリ使用量を計算し、それを元にmem_reqとs_vmemの２つのパラメータを設定している
        # Dockerを使う設定だとsoft-linkの指定先が読めない問題が発生するため、すべてのsoft-linkとその指定先を探し出してbindする
        # 実際にはSingluraityを呼び出すが、singularityはデフォルトでホームディレクトリをホストのものを使ってしまうので、使わないように設定しておく
        submit-docker = """
          set -xe
          IMAGE=$HOME/singularity-images/sha256/${docker}.sif
          MEM_G_PER_CORE=`echo 'scale=3; ${memory_gb}/${cpu}'|bc`G
	  mkdir -p ${cwd}/home

          binds=`find ${cwd} -type l|while read one; do
              real_path=$(realpath $one)
              link_path=$(readlink $one)
              echo -n '--bind' $real_path:$link_path:ro ' '
          done`

          qsub \
          -terse \
          -V \
          -b y \
          -N ${job_name} \
          -wd ${cwd} \
          -o ${out} \
          -e ${err} \
          -pe def_slot ${cpu} \
          -v JAVA_TOOL_OPTIONS="-XX:+UseSerialGC -XX:CICompilerCount=2" \
          -l mem_req=$MEM_G_PER_CORE,s_vmem=$MEM_G_PER_CORE \
          singularity exec --no-home -H ${cwd}/home --bind ${cwd}:${docker_cwd} --bind ${script}:${script}:ro $binds $IMAGE ${job_shell} ${script}
        """
        
        # 120秒おきにqstatを実行しジョブの実行状況を確認する
        exit-code-timeout-seconds = 120

        # Job IDのフォーマット
        # qsubの結果の標準出力がどのようになっているか指定する
        job-id-regex = "(\\d+)"

        # Jobをキャンセルしたいときに実行するコマンド
        kill = "qdel ${job_id}"
        kill-docker = "qdel ${job_id}"

        # Jobの実行状況を確認するためのコマンド
        check-alive = "qstat -j ${job_id}"

        # 同時に投入するジョブの数
        # ここを超えるジョブ数はcromwellのキューでまち状態になる
        concurrent-job-limit = 1000
      }
    }
  }
}
```

パスに通すシェルスクリプト
----------------------

SingularityにDockerのふりをさせるために以下のシェルスクリプトを`docker`という名前で保存し、パスを通します。また、必要なイメージは`docker pull`で取得すると適切な名前で`$HOME/singularity-images`以下に保存してくれます。インターネットに繋がっていない環境の場合、インターネットに繋がっている環境でこのスクリプトを利用して`docker pull`を実行し、`$HOME/singularity-images`を持ち込むとうまく動きます。


```
#!/bin/bash

set -e

IMAGE_DIR=$HOME/singularity-images/tag
IMAGE_DIR2=$HOME/singularity-images/sha256

export PATH=/usr/local/pkg/singularity/current/bin:${PATH}

#mkdir -p "${IMAGE_DIR}"

case $1 in
    "pull" )
        DOCKER_IMAGE="$2"
        IMAGE_PATH=${IMAGE_DIR}/${DOCKER_IMAGE}.sif
        if [ -e ${IMAGE_PATH} ];then
            echo "This image is exists. Please delete ${IMAGE_PATH} to recreate image"
        else
            mkdir -p ${IMAGE_PATH%/*}
            singularity build "${IMAGE_PATH}" "docker://${DOCKER_IMAGE}"
            sha256sum "${IMAGE_PATH}" > "${IMAGE_PATH}.sha256sum"

            IMAGE_WO_TAG=${DOCKER_IMAGE%:*}
            SHA256_CONTENT=$(cat "${IMAGE_PATH}.sha256sum")
            SHA256=${SHA256_CONTENT%% *}

            IMAGE_PATH2=${IMAGE_DIR2}/${IMAGE_WO_TAG}@sha256:${SHA256}.sif
            mkdir -p ${IMAGE_PATH2%/*}
            ln ${IMAGE_PATH} ${IMAGE_PATH2}
        fi
        ;;
    "images" )
        find "${IMAGE_DIR}" -name '*.sif'|while read line; do
            IMAGE_PATH=$(echo $line|sed -e "s|^${IMAGE_DIR}/||g")
            IMAGE_NAME=${IMAGE_PATH%.sif}
            BASE_NAME=${IMAGE_NAME%:*}
            TAG_NAME=${IMAGE_NAME#*:}
            SHA256_CONTENT=$(cat ${line}.sha256sum)
            SHA256=${SHA256_CONTENT%% *}
            printf "${BASE_NAME}\t${TAG_NAME}\tsha256:${SHA256}\n"
        done
        ;;
    "find" )
        DOCKER_IMAGE="$2"
        IMAGE_PATH=${IMAGE_DIR}/${DOCKER_IMAGE}.sif
        if [ -e ${IMAGE_PATH} ];then
            echo ${IMAGE_PATH}
        else
            echo "Image not found"
            exit 1
        fi
        ;;
    * )
        echo invalid command
        exit 1
        ;;
esac
```

ここまで設定すればWDLがGridEngineでDockerも含めて動くはずです。（あとでもうちょっと追記します）