ABCIシステムの使い方(応用編)

はじめに

ABCIシステムの使い方というタイトルでブログ記事を書いたがまぁまぁ反響があったため、自身が実際にどうスパコンの上で基盤構築したかをアプリケーション依存1の話を極力避けてお話したい。

なお、MPIやOpenMPについては説明しない。これについては、たとえば、 https://github.com/kaityo256/sevendayshpc/blob/master/day1/README.md とか https://portal.abci.ai/docs/ja/08/ とか https://portal.abci.ai/docs/ja/ngc/ とかを御覧ください。

お約束ですが、あくまで個人の使用感と知見なので、詳細かつ正確な動作については、公式のドキュメントを見ましょう。

Reference

準備

以下の手順を踏んだ:

  1. アプリケーション用の環境構築のためにdocker imageを作成する
  2. Docker2Singularityでdocker image -> Singularity Imageに変換する
  3. 1.と並行して、MySQLのSingularity Imageを作成する
  4. 2で作成したSingularity imageをAmazon S3に保存しておく
  5. (Ansibleで)ABCIのホーム領域スクリプト達やS3のcredentialとか(こちらはansible vaultで)を配置しておく

あとは、ABCI上で配置されたシェルスクリプトを実行すれば、3でAmazon S3に保存されたSingularity Imageをdownloadしてきて所要のプログラムを実行するだけで良い。この手順については、Reference[1]の「Singularity imageの作成方法」の4番目の方法を踏襲している。

Note) 今回は、次節で述べられている事情があり、Dockerfileから直接imageを作りに行ったので、かなりめんどくさいことをやっている。実際には、ABCIだと、既にtenforflowやpytorchなどがmodule loadコマンドで使用できる2ので気軽に使用できるはずだ。やりたい事に応じてABCIのドキュメントを適宜参照するのが一番良い。

Singularity imageの作成まで

自分の場合、PythonC++用のtensorflowを用意する必要があったので、Dockerfileを(わざわざ)自前で用意し、以下の手順で作成していた:

  1. https://www.tensorflow.org/install/source とかを参照してC++用のtensorflowの環境をDockerfileで作成する。Python用はDockerHubにあるが、C++用で所要のバージョンのものがなかったので、Dockerfileから作成した。自分の場合はFROM nvidia/cuda:9.0-cudnn7-devel-ubuntu16.04の上で構築した。
  2. アプリケーションを動作させるための環境もDockerfileで作成する。こちらはFROM knknkn/tensorflow(1番のやつ)の上で構築する。1と2でDockerfileを分離した理由としては、1のC++用tensorflowのbazelでのビルドが数時間かかる(!!)ので、できるだけ1の再ビルドを避けたいという意図から。
  3. アプリケーションのソースコードレポジトリのmasterへのコミットをトリガーにして、2で作成したDockerfileをビルドし、imageを作成する。このDocker imageを更にDocker2Singularityを用いてSingularity Imageに変換しておく。

1:のtensorflowのビルドの確認は、 https://www.tensorflow.org/install/lang_chttps://www.tensorflow.org/install/lang_c#example_program 辺りを見て確認すると良い。tensorflowのビルドは普通に実行すると、対話的に行われるが、これを避けるために、bazelの設定をhttps://gist.github.com/knknkn1162/3486df7604b50d16fc071c12aaeeb317 のようにし、以下のように設定ファイルをCOPYして、以下のようにビルドした:

# Dockerfile
# skip
# build tensorflow from source
ENV APP_PATH=/app
ARG PAR=4
ARG BAZEL_VERSION=0.18.1
ARG USR_LOCAL_PATH=/usr/local

RUN git clone -b r1.12 --recurse-submodules https://github.com/tensorflow/tensorflow
WORKDIR tensorflow
# equivalent to run ./configure and set iteratively
COPY ./tensorflow/ ${APP_PATH}/tensorflow
RUN bazel build -c opt -j ${PAR} --config=cuda --config=monolithic //tensorflow:libtensorflow_cc.so && \
  install -m 0644 bazel-bin/tensorflow/libtensorflow_cc.so ${USR_LOCAL_PATH}/lib/ && \
  # finally, we refresh shared library cache.
  ldconfig

.tf_configure.bazelrcの設定ファイルでCUDAに関する諸々を設定しているので、これでGPUサポートされている形となる。

Note) ABCIのノードの方でも(多分)

module load cuda/9.0
module load cudnn/7.3/7.3.1

としておく必要がある。

Note) もし、2のDocker imageをdocker上でGPUサポート有りで動かす場合は、 nvidia-docker2を用いる。こちらは、CentOS7かRedHat上でインストールする必要がある。インストール完了後は--runtime=nvidiaを用いてdocker run --runtime=nvidia -it --rm hogehoge/app /bin/bashと実行すれば良い。

Singularityの場合、--nvオプションがGPU仮想化に相当する。


Docker2Singularityは使い方は簡単で、以下のようにしてDocker imageをinputとしてSingularity Imageに変換している:

docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(PWD):/output --privileged -t --rm singularityware/docker2singularity hogehoge/app

MySQLのSingularity Imageの作成

こちらは、Reference[1]の「Singularity imageの作成方法」2番を参照した。SingulrityHubMySQLのimageがなく、既存のMySQLのDocker imageからSingularity Imageを作成しようとしてもうまく行かなかったため、一からSingularity Recipe(Dockerfileみたいなもの)を作成する必要があった。これについては、https://www.singularity-hub.org/containers/9521 にある。

なお、Singularity RecipeとDockerfileの書き方はかなり違うので、もし、Singularity Recipeを作る機会があったら頑張ってください😲

Ansibleでの配置

インタラクティブノードにスクリプト達を置く(というより、git cloneする)だけなので、特にAnsibleを使用する必然性はなかった3が、自分はReference[1]の「ABCIの概要」にも述べたように、sshのトンネリングを掘ってから、ansible-playbookを実行した。

デプロイ、プログラム実行まで

さて、Ansibleでいろいろスクリプトファイルを置ける準備が整ったので、後はscriptを実行して、所要のプログラムを実行させることができる。

次節ではABCIを便利に扱うことのできるコマンドをざっと紹介する。 その次の節ではABCI上で全体のインフラ基盤を設計する上で、「こうしたい -> こうした」という事例をいくつか紹介する。

便利なコマンド

これは実はReference[1]の「Job Scheduler」の章に既にだいたいまとめてある。Job Schedulerの基本的な使い方としては、https://portal.abci.ai/docs/ja/03/ を参照のこと。

Q&A

本節ではややプログラム依存になってしまうかもしれないが、自分の実装をもとにQ&A形式で「こうしたい -> こうした」的な感じでまとめる。

Note) https://portal.abci.ai/docs/ja/01/#system-use-overview に、「ABCIシステムでは、全ての計算ノードとインタラクティブノードは大容量ストレージシステム(DDN GRIDScaler)を共有します。」と書いてあることを確認すること。例えば、あるファイルをどっかのノードに置けば、そのファイルが全ノードに共有される。簡易なものを作成するときはこの性質を利用すれば事足りてしまう。

Singularityについて

SingularityとはHPC用のコンテナライブラリでDockerと同種のものだが、使用感がややDockerと異なる。基本的な使用法についてはReference[1]の「Singularity」の節を参照のこと。

SingularityコンテナでGPUを使いたい

A: singularityコマンドの--nvを指定する。 https://sylabs.io/guides/2.6/user-guide/faq.html?highlight=gpu#does-singularity-support-containers-that-require-gpus を参照すると良い。

任意の場所にファイルを配置したい

A: Singularityだと、--bindオプションが使用できる(dockerだと、--volumeに相当するコマンド)なので、それを使えば便利: singularity exec --bind ${OUTER_PATH}:${INNER_PATH} みたいな感じで。

MySQLスパコンの上で使用したい

A: そもそもスパコンでは、「全ての計算ノードとインタラクティブノードで大容量ストレージシステムを共有する」ので、使用する場面は限られるかもしれない。今回の場合、ルールが完結しているゲーム(囲碁)の強化学習で自動的に学習ファイルを大量に生成させる必要があり、その管理のためにMySQLを用いたという経緯がある。

MySQL daemonを立ち上げるためにbackgroundでコンテナを動かす必要がある。これについては、 https://sylabs.io/guides/2.6/user-guide/running_services.html?highlight=instance%20start#running-services で述べられているように、singularity instance.startsingularity run instance://...を用いる:

自分が作ったMySQLのsingularity Imageは https://www.singularity-hub.org/containers/9521 にある(shub://knknkn1162/mysqldでとってこれる):

MYSQL_CLIENT_SIMG_PATH=${SIMG_PATH}/mysql-client.simg
MYSQL_DATADIR=${MYSQL_BASEDIR}/mysql/var/lib/mysql
MYSQL_SOCKDIR=${MYSQL_BASEDIR}/mysql/run/mysqld

singularity pull docker://jbergknoff/mysql-client $(basename ${MYSQL_CLIENT_SIMG_PATH}) # pull mysql client image
mv $(basename ${MYSQL_CLIENT_SIMG_PATH}) ${MYSQL_CLIENT_SIMG_PATH}

mkdir -p ${MYSQL_DATADIR} ${MYSQL_SOCKDIR}
hostname -i | tee ${MYSQL_ADDR_FILE} # To use this on other nodes.

singularity pull --name $(basename ${MYSQL_SERVER_SIMG_PATH}) shub://knknkn1162/mysqld
mv $(basename ${MYSQL_SERVER_SIMG_PATH}) ${MYSQL_SERVER_SIMG_PATH}

singularity instance.start --bind ${JOBS_PATH}/mysqld/init/my.cnf:/my.cnf --bind ${MYSQL_DATADIR}:/var/lib/mysql --bind ${MYSQL_SOCKDIR}:/run/mysqld ${MYSQL_SERVER_SIMG_PATH} mysqld
# initialize mysqld and then launch mysqld
singularity run instance://mysqld

# wait until mysql server launch
for i in `seq 1 ${MAX_RETRY}`
do
  source ${JOBS_PATH}/mysql_client/ping
  if [ $? -ne 0 ]; then
    sleep 1
    echo -n "."
  else
    echo -n ' : success!'
    break
  fi
done
echo
sleep 5 # for safety

MySQLの場所を原始的にファイルで保存している(hostname -i | tee ${MYSQL_ADDR_FILE})。 スパコンのストレージは全ノード共有なので、このipアドレスが書き込まれたファイルは全ノードで共有されることになり、ipアドレスが全ノードに伝達されることになる。

singularity run instance://mysqldはSingularity Recipeのrun script4が実行終了したら処理を返すコマンドなので、MySQLサーバーが正常に実行されたか確認するために、source ${JOBS_PATH}/mysql_client/ping(簡易的にあUptimeが存在するかどうかをチェックしている)でエラーステータスを確認し、正常ならば次に進めるようにしている。

# source ${JOBS_PATH}/mysql_client/ping
source ${JOBS_PATH}/mysqld/constants

# check if the number of seconds exists that the server has been up.
singularity exec ${MYSQL_CLIENT_SIMG_PATH} mysql -u${MYSQL_USER} -h`cat ${MYSQL_ADDR_FILE}` -p${MYSQL_PASSWORD} -e "show global status like 'Uptime'"

全体設計

複数条件でプログラムを動かしたい

Q: 複数条件でプログラムを動かしたい(testA, testB, testCの条件でそれぞれプログラムを実行したい)場合。

A: 本質的にはqsub-hold_jidを用いることで、(testAの条件:100ノード) -> (testBの条件:100ノード) -> (testCの条件:100ノード)の順にシーケンシャルに実行できる。-hold_jidを使用しないと、全てが並列に進んでしまう(厳密に言うと、Job Schedulerのスケジュールのアルゴリズムに委ねられる)

# ABCI_GROUP, LOG_PATH, EMAIL_ADDRESSなどは適当に指定
alias qsubg="qsub -g ${ABCI_GROUP} -j y -cwd -terse -M ${EMAIL_ADDRESS} -m besa -o ${LOG_PATH} -e ${LOG_PATH}"
alias qsub_full="qsubg -l rt_F=1"

とした上で、

# exec.sh
CUR_JOB_ID=""
PREV_JOB_ID=1 # It's a dummy but valid number...
REPEAT=15

for i in `seq 1 ${REPEAT}`; do
  CUR_JOB_ID=$(qsub_full -hold_jid ${PREV_JOB_ID} program.sh)
  # do post-process
  # And then, update
  PREV_JOB_ID=$CUR_JOB_ID
done

とする。このようにすれば、前の処理が終われば、次の処理に移行するみたいな事ができるようになる。

-hold_jidoptionについて。-hold_jid ${PREV_JOB_ID}として、引数に数字を指定する。この場合、もし今queueに積まれているjobの中にPREV_JOB_IDがなければ無視される。最初にPREV_JOB_ID=1とする5ことで、job_id=1のjobが存在しないことを仮定しているので、0番目のqsub実行時でもうまくいく。

シーケンシャルな実行はかなり便利で、他にも最初期に一度だけ色々なファイルのinitializationをしたい場合などに使える。

Note) ちなみに、qsubで投げられるjobは終了するまでqueueに貯められる(qstatコマンドでチェックできる)が、この上限は2019/7現在 1000になっている。普通、-hold_jidなんてものを用いずにqsub実行する分には上限数1000は十分すぎるほど大きいが、この小節のような要求があった場合は、1000以上必要になるかもしれない。 自分の場合はABCIの管理者に上限を増やしてください的なお願いをしたが、ちょっと増やすのは他のお客様に迷惑がかかるのでできない、と断られてしまった。

ジョブをスキップしたい

Q: ⇑を踏まえた上で、ある条件で動かしていたプログラムの結果がだいたい把握できたので終了して、次の条件でプログラムを実行させたい

A: 自分の場合はフラグを書き込むファイルにて管理していた。終了するときはqdelコマンドを使えばノードがどのような状態であっても問答無用でシャットダウンできるが、生成中のファイルが破損する可能性があるため、定期的にプログラムを切りの良いところで終了させて、チェックするようにしていた

FILE="~/switch"

for i in `seq -f %05g 99999`
do
  # termination condition mets, break loop!
  if [ $(cat ${FILE}) -eq 1 ]; then
    break
  fi

  singularity exec --nv ${SIMG_PATH} /bin/bash -xc "./program"
  if [ $? -ne 0 ]; then
    echo "error"  
  fi
done

singularity exec内部で起動するプログラムのサイクルを5~10分程度の用に短くすれば、FILE="~/switch"の内容を(インタラクティブノードで)1に変更するだけで./programで生成させるファイルを壊さずに、このノードを終了させることができる。即座にノードが終了するわけではないのが欠点だが、いわゆるGraceful shutdown的なことができる。

スポットの計算ノードを使用したい

Q: 予約ノードのほうが1.5倍料金かかるので、Spotで計算ノードを使用したい6

A: 予約ノードを利用する利点としては、30日間連続で専有できることである。一方で、Spotでは72時間の連続利用に限られる(だから価格がある程度安い)。自身のプロジェクトでは、72時間以内で(もう少しきつく60時間以内で)ノードを終わらせて、再度起動するように工夫していた。実装方法としては、タイマーを設けて(例えばdate "+%s"の結果を用いるとか)、"ジョブをスキップしたい"の実装のようにループで定期的に確認するようにするのが良いだろう。

予約IDのqsubへの割当をうまいことしたい

Q: 一度に予約でAR id(archive reservation id)が一つ発行されるが、一度に予約できるノードが32ノード上限なので、それ以上のノード数を取り扱う場合、AR idを複数管理する必要がある。なんかいい感じにうまいことできないか? 例えば、50ノードを予約したいときは、AR idを2つ管理する必要があり、qsubでの実行時にar_idの指定をするのがやや面倒..

A: まぁ、普通の人は33ノード以上を使用すること場面はかなり限られるかもしれないが、もしその必要性に駆られた場合。これは自分はshell scriptで無理やりやったので実装が汚い&もうちょっといい実装がありそうなのを承知で。。

基本的なアイデアとしては、qrstatコマンド( https://portal.abci.ai/docs/ja/03/#show-the-status-of-reservations )の-xmlオプションを用いて予約ノードのIDと予約ノード台数をget_ar_infoで抽出し、AR_ID_LISTSでリスト化した出力を格納し、実際に使用するときはget_ar_idnext_ar_idでイテレートしていく:

_FILTER_TAG='s/<[^>]*>//g'
_FILTER_TAG_SPACE='s/<[^>]*>/ /g'
_FILTER_ATTR='s/^.*"\(.*\)".*$/\1/'

function get_ar_info() {
  local QRSTAT_XML=`qrstat -xml`
  local AR_ID_XPATH="//qrstat/ar_summary"
  local AR_COUNT=$(echo $QRSTAT_XML | xmllint --xpath "count($AR_ID_XPATH/id)" -)

  # check r state
  for idx in `seq 1 $AR_COUNT`; do
    local AR_ID=$(echo $QRSTAT_XML | xmllint --xpath "$AR_ID_XPATH[$idx]/id" - | sed -e ${_FILTER_TAG})
    local AR_STATE=$(echo $QRSTAT_XML | xmllint --xpath "$AR_ID_XPATH[$idx]/state" - | sed -e ${_FILTER_TAG})
    if [ $AR_STATE = "r" ]; then
      local SLOTS=$(qrstat -ar ${AR_ID} -xml | xmllint --xpath "count(//qrstat//ar_summary/granted_slots_list/granted_slots)" -)
      echo "$AR_ID,$SLOTS"
    fi
  done
  # check E(running with error) state.
  for idx in `seq 1 $AR_COUNT`; do
    local AR_ID=$(echo $QRSTAT_XML | xmllint --xpath "$AR_ID_XPATH[$idx]/id" - | sed -e ${_FILTER_TAG})
    local AR_STATE=$(echo $QRSTAT_XML | xmllint --xpath "$AR_ID_XPATH[$idx]/state" - | sed -e ${_FILTER_TAG})
    if [ $AR_STATE = "E" ]; then
      local SLOTS=$(qrstat -ar ${AR_ID} -xml | xmllint --xpath "count(//qrstat//ar_summary/granted_slots_list/granted_slots)" -)
      local ERRS=$(get_error_instances $AR_ID | xargs -n1 echo | wc -l)
      local SLOTS=$(($SLOTS - $ERRS))
      echo "$AR_ID,$SLOTS"
    fi
  done

  return 0
}

function get_error_instances() {
  local AR_ID=$1
  qrstat -ar $AR_ID -xml | xmllint -xpath "//qrstat/ar_summary/message" - | sed -e 's/^.*\(gpu@g[0-9]\{4\}\).*$/\1 /g'
}

AR_LISTS=$(get_ar_info)

# If AR_LISTS are empty, create zero_id list which means non-reserved in ABCI system.
# Otherwise, AR_LIST are not empty, append zero_id list.
AR_LISTS="$AR_LISTS 0,300"

AR_ID_LISTS=$(echo $AR_LISTS | tr , ' ' | xargs -n2 /bin/bash -c 'seq 1 $1 | xargs -I{} echo -n "$0 "')
# restore ar_id from file
_AR_ID_IDX=1

function get_ar_id() { # get element as iterator
  echo $AR_ID_LISTS | cut -d ' ' -f ${_AR_ID_IDX}
  return 0
}

function next_ar_id() {
  _AR_ID_IDX=$(( 1 + $_AR_ID_IDX ))
  return 0
}

たま~に予約時に予約できないノードが発生する場合があることが確認できた7。その処理も追加で書いているため、get_ar_infoが複雑になっている。xml形式でデータが取れるので、xmllint -xpathsedで頑張ってparseしてました。

実際にqsubで使用する時:

_H_RT="rt_F=1" # select node full occupation
CONST_H_RT="h_rt=72:00:00"
_AR_ID=`get_ar_id`
[ $_AR_ID -eq 0 ] && _H_RT=$CONST_H_RT
qsub_full -hold_jid ${PREV_JOB_ID} -ar $_AR_ID -l ${_H_RT} ./program
next_ar_id

qsub_fullは「複数条件でプログラムを動かしたい」の小節で定義されている独自コマンド。 qsubの-arは0の時、-arが指定されていない場合と同等の挙動をする(これは裏仕様だと思う)のを逆用して、

  • ar_id = 0のとき -> [予約ノードが全て使用されている or 予約ノードがそもそもない]ので、スポットのノードを用いる
  • ar_id !=0の時 -> 予約idのノードを用いる

みたいな少しトリッキーなことを行っている。

1ノード4GPUあるので、1プロセス1GPUでプログラムを走らせたい

A: CUDA_VISIBLE_DEVICES=${number}をこの様に用いれば良さげ。

singularity exec --nv ${SIMG_PATH} /bin/bash -xc "CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES_ID} python ./param_gen"

この様なスクリプトをバックグラウンドで4つ実行すればよい。ただし、CUDA_VISIBLE_DEVICES_ID=0,1,2,3のようにしておく。

すべての自身が立てたqueue数を知りたい

A: qrstatを用いる:

function get_queue_count() {
  qstat -xml | xmllint --xpath "count(//job_info//job_list)" -
}

予約ノード以外のノードが使用されているかをチェックする

Q: 予約ノード以外のノードが使用されているかをチェックする。予約したのに、Spotのインスタンスが使われている場合がないかどうか確認したい

A: qrstatを用いて、下のget_nonreserved_running_instancesコマンドを実行すると良い。

_FILTER_TAG_SPACE='s/<[^>]*>/ /g'

function get_nonreserved_running_instances() {
  local AR_LIST=`get_ar_instances`
  local RUNNING_LIST=`get_running_instances`
  for ins in $RUNNING_LIST; do
    [[ "$AR_LIST" =~ $ins ]] || echo $ins
  done
}

function get_ar_instances() {
  for ar_id in `qrstat -xml | xmllint --xpath "//qrstat/ar_summary/id" - | sed -e "${_FILTER_TAG_SPACE}"`; do
    qrstat -ar $ar_id -xml | xmllint -xpath "//qrstat/ar_summary/granted_slots_list/granted_slots/@queue_instance" - | sed -e 's/queue_instance=//g' | sed -e 's/"//g' | xargs -n1 echo
  done
}

function get_running_instances() {
  local QSTAT_XML=$(qstat -xml)
  echo ${QSTAT_XML} | xmllint --xpath "//job_info/queue_info/job_list/queue_name" - | sed -e "${_FILTER_TAG_SPACE}" | xargs -n1 echo
}

終わりに

自身もスパコンを2019年の1月から使い始めたばかりの初心者なのですが、以下の点で面白かったです:

1: 100ノード400基のTesla V100 GPUのリソースを2ヶ月半も使用、専有していると、全能感を味わえる。頭がバグる。

2: 環境構築が割と特殊(C++のtensorflowの用意)だったので、スクラッチでインフラのための環境構築を実装し、各ノードの管理もスクラッチで実装した。

3: GPUの性能が極めて高いため、1ノードでもかなりのことができる。ノード数が莫大になる(数千~数万台)事による従来のスパコン特有の面倒な問題を、GPUスパコンを用いることで回避することができる。

4: 人は平日の10時から6時までしか働けないが、コンピュータは(メンテナンスを除けば)365日24時間働かせられるので、コンピュータの部下をたくさん抱えてる気持ちになる(未来に生きてる感じがする)。

ABCIは現時点ではスパコンの京よりは知名度が低いですが、2019.6月時点で(公開されている中で)日本最速のスパコン8ですし、法人での申込が可能9で、他のクラウドベンダーから借りる場合よりも格安で使用できる10ので、一度お試しでも使ってみてはいかがでしょうか。


  1. 具体的には、https://www.globis.co.jp/news/release/20190418_globis.html の社内プロジェクト(囲碁AI)で自身がABCIのスパコンを(100ノード400基のTesla V100 GPUを)用いて環境構築したお話です。

  2. https://portal.abci.ai/docs/ja/11/

  3. AWS上のインスタンスで事前に環境を構築していたのもあって、Ansibleを選択していた(作成した当初はスパコンの構成が見えていなかった、というのもある)

  4. Singualrity Recipeにおいて、%runscript以下のコードがsingularity run実行時に実行される。%runscriptに関しては、https://sylabs.io/guides/2.6/user-guide/container_recipes.html?highlight=runscript#runscript を参照のこと。

  5. job_idは1から始まるようだ。(0とするとうまく行かなかった)

  6. ABCIの利用料金については、 https://abci.ai/en/how_to_use/tariffs.html を参照のこと。

  7. Reference[1]のqrstatの小節の最後の結果を見てください。

  8. https://www.sankei.com/economy/news/190617/ecn1906170009-n1.html とか https://www.top500.org/lists/2019/06/

  9. https://abci.ai/ja/how_to_use/custom.html

  10. http://cstmize.hatenablog.jp/entry/2019/04/18/ABCI%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9 の「ABCIを使うにあたってのメリット・デメリット」を参照のこと。