ひとつの仕事、たくさんの部分
写真を明るくするには、何百万個もの画素に同じ小さな数を足します。ひとつの画素はほかの画素に左右されないので、何千もの小さな働き手がそれぞれ少しずつ受け持てます。グラフィックスカードは、まさにそのために作られています。このラボにはグラフィックスカードはありませんが、その土台になっている考え方を見ることができます。
このレッスンが終わると
メモ
たくさんのデータに同じ仕事を一度にさせることがループとどう違うか、そして働き手を増やしても仕事がその分だけ速くなるとは限らない理由を、説明できる。
キーワード
- 並列:仕事のいくつかの部分を同時に行うこと。
- ワーカー:仕事の一部を受け持つひとつのプロセスやスレッド。
- 直列の部分:分けることができず、順番に行わなければならない仕事の割合。
- 高速化率:働き手がひとりのときと比べて、仕事が何倍速くなるか。
- スレッド:小さな働き手。GPU は何千個も同時に動かす。
- カーネル:各スレッドが実行する小さな関数。スレッドはそれぞれ自分の要素に対して実行する。
はじめに:すでに知っていること
メモ
頭の中で答えてください。1. for ループは何をしますか。2. 1人が80枚のチラシを折るのに8分かかるとき、山を等しく分ければ4人では何分ですか。3. その8分のうち1分が、ひとりしかできない宛名リストの作成に使われるとしたら、どうなりますか。答え: 同じ手順を順番に繰り返す。2分。2分より長い。宛名リストは、ひとりで1分かかるままだから。
始める前に
メモ
レッスンの上のバーにある ラボを開始 を押し、動いていると表示されるまで待ちます。コンテナはひとつ、jupyter だけで、ターミナルからは docker exec jupyter でその中の Python を使います。これはコンテナの中でコマンドをひとつ実行する命令です。JupyterLab のリンクを押すと、ポート 8888 の画面が開きます。ここには GPU はありません。下の 実行 ボタンは、それぞれのコマンドをラボのターミナルに、同じセッションの中で順番に入力します。様子を見たいときや自分で入力したいときは、バーの ターミナルを開く(新しいタブ) を押します。
並列計算の作業は、小さなスクリプトのフォルダー
ls で作業フォルダーの中身を表示します。ノートブックの start.ipynb と、フォルダーの steps があります。cat は最初のスクリプトを表示します。1から1000までの2乗の合計を2通りで求めます。ひとつは Python のループ、もうひとつは NumPy の1回の操作で、配列全体に同じ仕事を一度に行います。
docker exec jupyter lsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
docker exec jupyter cat steps/vectors.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
配列全体に1回の操作
どちらの方法でも 333833500 になり、スクリプトは2つの結果が等しいと表示します。ループは Python の操作を2000回使います。1000個の数それぞれに、掛け算と足し算がひとつずつです。NumPy は1回です。1回の呼び出しで、速いコンパイル済みのコードが全部の仕事をするからです。時間は測らないので、スクリプトは毎回同じものを表示します。GPU はさらに進んで、1つの命令で何千もの要素を扱います。
docker exec jupyter python3 steps/vectors.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
仕事をワーカーで分ける
1から40までの数を10個ずつ4つのかたまりに切り、2つのワーカーで分担します。かたまりごとに部分和が出ます。55、155、255、355で、いつもかたまりの順に並びます。足すと820になり、リスト全体をまとめて足した結果と同じです。分けられるのは、各かたまりがほかを知らなくても足せるからです。
docker exec jupyter python3 steps/chunks.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
高速化の限界
どんな仕事にも、分けられない部分があります。表は、分けられない割合が10%、25%、50%のとき、ワーカーが1、2、4、8個で何倍速くなるかを計算します。直列が10%なら、ワーカー8個で4.71倍、8倍にはなりません。ワーカーが無限にいても、せいぜい10.0倍です。直列が50%なら、8個でも1.78倍です。
docker exec jupyter python3 steps/amdahl.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
確認
ヒント
表で、分けられない割合が25%のとき、ワーカー8個で仕事は何倍速くなりますか。答え: 2.91倍。8倍ではありません。直列の4分の1は、いつもひとつのワーカーが行うからです。
スレッドのブロックの起動
GPU は カーネル という関数を、スレッドごとに1回ずつ実行します。スレッドはブロックにまとまっていて、各スレッドは ブロック番号 * ブロックの大きさ + スレッド番号 で自分の要素を求めます。このスクリプトは、4スレッドのブロック3つを10個の要素に対して起動し、2つのベクトルを足す様子を再現します。各スレッドの全体での番号を0から11まで表示し、結果として 11、22、と続いて110まで表示します。最後の2つのスレッド、10と11は終わりを過ぎているので、何もしません。
docker exec jupyter python3 steps/kernel.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
データを動かすのにも時間がかかる
装置には限界が2つあります。バイトを動かす速さと、計算をする速さです。このスクリプトは、1秒に500ギガバイトを動かし、1秒に10テラフロップスの計算をする、架空の装置についての算数です。1000万要素のベクトルの足し算は、1000万回の計算のために 120000000 バイトを動かすので、メモリで限られます。1000×1000の行列の掛け算は、2000000000 回の計算のために 12000000 バイトを動かすので、計算で限られます。
docker exec jupyter python3 steps/transfer.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある思い込み
注意
ワーカーが8個あれば仕事が8倍速くなる、と思われがちです。そうなるのは、順番に行う部分がまったくないときだけです。直列のまま残る部分が上限を決め、データを動かす時間が計算より長くなることもあります。ワーカーを増やす前に、2つのことを考えましょう。分けられない部分はどれだけか。仕事はメモリを待っているのか、計算を待っているのか。
同じ作業をノートブックで
バーから JupyterLab を開き、start.ipynb をダブルクリックします。各セルがスクリプトをひとつずつ実行するので、6つのスクリプトがしたことを、ひとつの考え方ずつ見られます。セルをクリックして Shift と Enter を押すと実行されます。試してみるには、ファイルの一覧からスクリプトを開き、かたまりの数や分けられない割合などの数をひとつ変えて、もう一度実行します。
最後の確認
ヒント
前を見ないで答えてください。1. 2乗の合計で、ループは Python の操作を何回使い、NumPy は何回使いましたか。2. 4つの部分和は合計でいくつになりましたか。3. 直列が10%のとき、ワーカーを何個にしても得られる高速化の上限は何倍ですか。4. カーネルの中で、各スレッドが最初に求めるものは何ですか。答え: 2000回と1回。820。10.0倍。自分の要素がどれか、つまり全体での番号。
先生へ
ヒント
このレッスンは、ラボで進めるレッスンの小さな例です。手順と実行ボタンがレッスンで、ラボは生徒ひとりひとりのコピーです。ラボはサービスがひとつで、NumPy の入った JupyterLab です。GPU はなく、GPU の土台になっている考え方を教えます。自動採点はありません。生徒は、スクリプトのひとつに自分で加えた短い変更と、何がどう動いたかの一文を提出します。学習のポイント:1. 配列全体への1回の操作がループの代わりになる。2. ワーカーで分けた仕事にも直列の部分が残り、それが上限になる。3. カーネルはスレッドごとに1回実行され、各スレッドが自分の要素を求める。よくある思い込み:ワーカー8個で8倍の速さになると考えること。用意するもの:ラボとブラウザー。ヒント:実行を押す前に、表の各数を予想させてください。時間は測らないので、どのスクリプトも毎回同じものを表示します。ご自分のモジュールにコピーして変更するか、レッスンビルダーのラボメニューから一から作ってください。