実施要項(講義で配布したもの)

第4回数理工学プログラムコンテスト
概 要
C 言語によるプログラミングの応用練習を行う場として,プログラムコンテストを開催する.本資料
ではまず並列計算システムを利用したプログラミングの基礎を解説する.その後コンテストの問題を提示
し,その問題を解く方法の一つとして,汎用的な探索手法である遺伝的アルゴリズムを説明する.これら
の基礎知識を修得し,プログラミングに慣れるためにも,コンテストには是非とも多くの学生に参加して
いただきたい.
1
1.1
並列計算の基礎
今回利用する並列計算システムの構成
総合校舎 202 室に設置されている並列計算システムは,1 台のメインサーバと 8 台の計算ノードで構成されている(図
1).計算ノードは,一般に利用されている PC(パーソナル コンピュータ)と同等の性能を持つマシンである.メイン
サーバはプログラムファイルを保持し,ユーザからプログラムの実行要求を受けたときはそれぞれの計算ノードにプロ
セスを割り当てる働きをする.プロセスとは,OS(オペレーティング システム)が実行中のプログラムを管理する単位
のことであり,Linux や Windows では 1 つのマシン上に複数のプロセスを持つことができる(すなわち複数のプログ
ラムを同時実行できる).
協調して動作するマシンの集まりのことをクラスタと呼ぶ.今回利用する並列計算システムのように,安価なマシン
を複数利用して高速に並列計算を行うものを,とくに Beowulf 型クラスタと呼ぶ.この技術によれば,非常に高価な
スーパー コンピュータと同等の計算性能を比較的低コストで実現することも可能である.
図 1: 並列計算システム
1
1.2
MPI とは
今回利用する並列計算システムは分散メモリ型 MIMD システムと呼ばれる.分散メモリ型とは,複数の CPU がそ
れぞれ独立なメモリを持つシステムのことである1 .また,MIMD (Multiple Instruction/Multiple Data) とは,複数の
CPU が複数の異なるデータを並行処理する方式のことである2 .
分散メモリ型の並列計算システムでは,他の CPU に接続されているメモリに直接アクセスすることはできない.そ
のため,他の CPU が計算したデータが必要なとき(たとえば各 CPU での計算結果をとりまとめて最終的な計算結果
を出力するとき)は,通信によってデータを送受信しなければならない.この通信を実現するために一般に用いられて
いるのが MPI (Message Passing Interface) という規格である.今回利用するシステムには,この規格に沿って作られ
た MPICH というライブラリ(ソフトウェア)がインストールされているので,以下ではこれを利用して MPI プログ
ラミングを行う.
1.3
並列計算システムの利用法
本節では,並列計算システム上でプログラムを実行するための手順を一通り説明する.
1.3.1
ソースファイルの作成
まず,並列実行させるプログラムのソースファイルを作成する.MPI では,同じプログラムを複数のマシンで同時
に実行するので,用意するプログラムは 1 つだけでよい.ここでは,試しに以下のような簡単なプログラムを作成し,
hello.c というファイル名で保存することにする.なお,プログラムの内容については後に詳しく説明する.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
#include <stdio.h>
#include <string.h>
#include <mpi.h>
int main(int argc, char *argv[])
{
int myRank;
/* 自プロセスのランク */
char message[256];
/* 送受信するメッセージを格納するバッファ */
MPI_Init(&argc, &argv);
/* MPI 初期化処理 */
MPI_Comm_rank(MPI_COMM_WORLD, &myRank);
/* 自プロセスのランクを取得 */
if (myRank != 0)
/* ランク 0 以外のプロセスの処理 */
{
sprintf(message, "Hello, my rank is %d.", myRank);
/* 送信するメッセージを作成 */
MPI_Send(message, strlen(message) + 1, MPI_BYTE, 0 /* 送信先プロセスのランク */,
0, MPI_COMM_WORLD);
/* ランク 0 のプロセスにメッセージ送信 */
}
else
/* ランク 0 のプロセスの処理 */
{
int numProcess;
/* 全プロセス数 */
int sourceRank;
/* 送信元プロセスのランク */
MPI_Comm_size(MPI_COMM_WORLD, &numProcess);
/* 全プロセス数を取得 */
for (sourceRank = 1; sourceRank < numProcess; sourceRank++)
{
MPI_Recv(message, sizeof(message), MPI_BYTE, sourceRank, 0, MPI_COMM_WORLD,
MPI_STATUS_IGNORE);
/* ランク 0 以外のプロセスからメッセージ受信 */
puts(message);
/* 受信したメッセージを画面に表示 */
}
1 これに対し,共有メモリ型では複数の
2 これに対し,1
CPU でメモリを共有する.
つの命令が複数のデータに対して同時に並列処理を行う方式のことを SIMD (Single Instruction/Multiple Data) と呼ぶ.
2
33:
34:
35:
36:
37:
38: }
1.3.2
}
MPI_Finalize();
return 0;
/* MPI 終了処理 */
/* プログラム終了 */
コンパイルと実行
MPI を利用したプログラムをコンパイルするには,まずメインサーバ(ホスト名 expmain)にログインしなければな
らない.ログインするには,Cygwin のターミナルから
$ ssh expmain
というコマンドを実行すればよい.その後,mpicc というコマンドを利用してプログラムのコンパイルを行う.たとえ
ば hello.c をコンパイルして実行ファイル hello を作成するには,次のようにする.
$ mpicc -o hello hello.c
このプログラムを複数のマシン上で同時に実行して並列計算をさせるには,mpirun というコマンドを使えばよい.た
とえば
$ mpirun -np 8 hello
を実行すると,8 台の計算ノードに 1 つずつプロセスを割り当てて,同時に実行を開始する.
「-np 8」は起動するプロ
セス数の指定で,通常は全計算ノード数と一致させる.
プログラムにコマンドライン引数を与える場合は,最後に書けばよい.たとえば「 40」という引数を与えてプログラ
ムを実行するには,
$ mpirun -np 8 hello 40
とする.
1.4
MPI プログラミング
本節では,MPI プログラミングの基礎を解説する.より詳しく知るには別途配布する資料や,本,Web サイトなどを
適宜参照すること.
1.4.1
プロセスのランク
すでに述べたように,MPI では各マシンで同じプログラムを実行する.このとき,それぞれのプロセスには 0 からの
通し番号が付けられる.この番号のことをプロセスのランクと呼ぶ.ランクに応じて処理内容が変わるようにプログラ
ミングすれば,役割分担をさせることができる.ランク 0 のプロセスには,各プロセスでの計算結果を集計して画面に
表示するなど,リーダ的な役割を与えることが多い.このように全プロセスを統率する役割を果たすプロセスのことを,
マスタ プロセスと呼ぶ.
1.4.2
基本的な MPI 関数
ここでは,並列計算をさせる上で最低限必要な関数を挙げる.hello.c 内で使われている場所も示しているので,
hello.c を参照しながら使い方を理解してほしい.なお,MPI 関数を利用するには mpi.h をインクルードする必要が
ある.
• MPI_Init(&argc, &argv ); [10 行目]
初期化処理を行う.MPI を利用したプログラムでは最初に必ずこれを呼び出さなければならない.main() 関数
が受け取ったパラメータをそのままポインタ渡しする.なお,コマンドライン引数を利用する場合は,この関数を
呼び出した後に argc ,argv を参照すること.
• MPI_Finalize(); [35 行目]
終了処理を行う.MPI を利用したプログラムを終了させる直前に必ず呼び出す.
3
• MPI_Comm_rank(MPI_COMM_WORLD, &myRank ); [12 行目]
自プロセスのランクを取得して myRank (int 型)に格納する.通常はこのランクの値で処理を分岐させる.
• MPI_Comm_size(MPI_COMM_WORLD, &numProcess); [25 行目]
全プロセス数を取得して numProcess (int 型)に格納する.実行時に mpirun へのオプションとして「-np 8」
を与えた場合は 8 が格納される.この関数を利用して,全プロセス数が何個であっても正しく動作するようにプロ
グラミングするのが望ましい.
• MPI_Send(message, strlen(message) + 1, MPI_BYTE, destinationRank , 0, MPI_COMM_WORLD);
[17∼18 行目]
文字列 message をランク destinationRank のプロセスに送信する.文字列以外のデータを送信することもでき
る.その場合は第 1 引数としてデータの先頭を示すポインタ,第 2 引数としてデータの長さを与えればよい.た
とえば int 型の変数 data の内容を送信するには,
「MPI_Send(&data, sizeof(data), …);」とする.data が
double 型や構造体などであってもまったく同様である.
• MPI_Recv(message, sizeof(message), MPI_BYTE, sourceRank , 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
[29∼30 行目]
ランク sourceRank のプロセスから送信された文字列を受け取り,message(char 型配列)に格納する.MPI_Send()
と同様に,文字列以外のデータを受信することもできる.第 1 引数はデータを格納するバッファの先頭を示すポイ
ンタ,第 2 引数は受け取るメッセージの最大長(すなわちバッファ サイズ)である.なお,指定したプロセスか
らのメッセージがまだ到着していない場合は,到着するまで待つ.
1.4.3
サンプルプログラムの解説
hello.c の動作は以下の通りである.
ランク 0 以外のプロセス:
"Hello, my rank is #." という文字列メッセージをランク 0 のプロセスに送信する(# は自プロセスのラ
ンク).
ランク 0 のプロセス:
ランク 1 のプロセスから順番に,他の全プロセスから 1 回ずつメッセージを受け取り,画面に表示する.
したがって,プログラムの実行結果は以下のようになる.
Hello,
Hello,
Hello,
Hello,
Hello,
Hello,
Hello,
my
my
my
my
my
my
my
rank
rank
rank
rank
rank
rank
rank
is
is
is
is
is
is
is
1.
2.
3.
4.
5.
6.
7.
4
1.4.4
非同期通信(ノンブロッキング通信)
MPI_Recv() では,指定したプロセスからのメッセージが到着していない場合,到着するまで待つ.最後に 1 回だけ
(計算結果を集計するための)通信を行うプログラムであればこれで問題はない.しかし,相互に何度も通信しながら計
算を進めるプログラムや,いずれかのプロセスで解が見つかった時点で全プロセスを終了させるプログラムなどでは,受
信待ちによるプログラムの実行中断が不都合となる場合がある.このような場合には,MPI_Irecv() を使えばよい3 .こ
れはノンブロッキング受信を行う関数で,指定したプロセスからのメッセージがすでに到着しているかどうかに関係な
く,呼び出し後すぐに戻ってくる.これを呼び出した後 MPI_Test() により定期的にメッセージが到着したかどうか確
認して,到着した場合は MPI_Irecv() の呼び出し時に指定した受信バッファを参照し,受け取ったメッセージに関する
処理を行うようにすれば,他の処理をしながら他プロセスからのメッセージを待ち受けることが可能である.それぞれ
の関数の呼び出し方は以下の通りである.
• MPI_Irecv(message, sizeof(message), MPI_BYTE, sourceRank , 0, MPI_COMM_WORLD, &request);
ノンブロッキング受信を行う.最後の引数以外は MPI_Recv() と同様である.request は MPI_Request 型の変
数で,MPI リクエスト ハンドラと呼ばれる.この関数を呼び出すと,受信要求を識別する情報がこの request に
格納される.この情報は次の MPI_Test() で必要となる.
• MPI_Test(&request, &complete, MPI_STATUS_IGNORE);
MPI_Irecv() で受信要求をしたプロセスからメッセージが到着したかどうかを int 型変数 complete に格納す
る(到着していれば非 0,到着していなければ 0).request は上述の MPI リクエスト ハンドラである.
3 マルチスレッドにすることによって解決する方法もある.
5
コンテスト募集要項
2
2.1
問題
次の条件を満たす,コマンドライン引数で指定された長さ以上の文字列を1つ表示して終了する 並列計算 プログラムを
作成せよ.
• 26 種類の大文字アルファベット(A∼Z)で構成される
• 自然数 m, n (m < n) をどのように選んでも,
『文字列の m 番目の文字と (m + p) 番目の文字が同じで,かつ n 番目の文字と (n + p) 番目の文字も同じである』
ような自然数 p が存在しない
たとえば,文字列 “ABACDC” はこの条件を満たさない(図 2).なぜならば,1 番目の文字と (1 + 2) 番目の文字が
同じ (A) で,かつ 4 番目の文字と (4 + 2) 番目の文字も同じ (C) だからである(m = 1, n = 4 と選んだとき,問題文
中の『 』内を満たす自然数 p = 2 が存在する).逆に文字列 “ABACCA” は問題の条件を満たす.
2.2
提出方法
プログラムのソースファイルと,アルゴリズムの説明などをまとめたレポートを,電子メールに添付して松本先生
([email protected])と,本コンテスト TA の遠藤([email protected])に送信すること.レポートは
テキストファイル,PDF ファイル,Word ファイル,dvi ファイルのうちいずれかの形式で作成し,自分で工夫した点
に関して特に詳しく記述すること.
締切は 平成 17 年 10 月 31 日(月)17 時 とする.
2.3
評価方法
計算速度,アイデア,レポートの質などについて総合的に評価を行う.
2.4
質問の受付
下記の掲示板で,本コンテストに関する質問等を受け付ける.
http://free1.principle.jp/cbbs/mpicontest/cbbs.cgi
電子メールで遠藤([email protected])に質問しても構わない.
図 2: 条件を満たさない文字列と満たす文字列
6
ヒント
3
3.1
ランダム探索
本コンテストのような探索問題を解くために利用できる探索アルゴリズムとしては,まず全数探索,ランダム探索が
挙げられる.全数探索とは,すべての場合を順次調べることにより条件を満たす解を探索する方法である.また,ラン
ダム探索とは,解をランダムに生成し,それが条件を満たすかどうかを調べていく方法である.これらの方法はシンプ
ルだが,効率は一般に良くない.
ランダム探索によって本コンテストの問題を解く シングル CPU 用 のプログラム例を以下に示す.このプログラムは,
条件を満たす文字列が見つかるまで,ランダムに文字列生成を繰り返すものである.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<time.h>
#define MyRand(sup) ((int)((double)rand() * (sup) / (RAND_MAX + 1.0)))
#define MAX_LEN_STR 100
/* 生成文字列の最大長 */
/* [0, sup) の整数乱数を生成 */
/* numChara 種類の文字からなる長さ len の文字列をランダムに生成して buf に格納 */
void GenerateRandomString(char *buf, int len, int numChara)
{
int i;
for (i = 0; i < len; i++)
buf[i] = ’A’ + MyRand(numChara);
buf[i] = ’\0’;
}
/* 文字列 str が問題の条件を満たしていれば 1,そうでなければ 0 を返す */
int CheckString(char *str)
{
int m, n, p;
int len = strlen(str);
for (m = 0; m < len - 2; m++)
/* C 言語では添字が 0 から始まることに注意 */
{
for (n = m + 1; n < len - 1; n++)
{
for (p = 1; p < len - n; p++)
{
if (str[m] == str[m + p] && str[n] == str[n + p])
return 0;
}
}
}
return 1;
}
/* numChara 種類の文字からなり,問題の条件を満たす長さ len の文字列を探して buf に格納 */
void FindString(char *buf, int len, int numChara)
{
while (1)
{
7
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
3.2
GenerateRandomString(buf, len, numChara);
if (CheckString(buf))
return;
}
}
int main(int argc, char *argv[])
{
int len;
char str[MAX_LEN_STR + 1];
srand(time(NULL));
if (argc != 2 || (len = atoi(argv[1])) > MAX_LEN_STR)
{
puts("Error!");
return 1;
}
FindString(str, len, 26);
puts(str);
return 0;
}
遺伝的アルゴリズム
ランダム探索より探索効率を上げるには,それぞれの問題に適合したアルゴリズムを考えればよい.しかし,良いアル
ゴリズムがなかなか思いつかない場合もある.そのような場合は,ほとんどどのような問題にも適用できる汎用的な探
図 3: 遺伝的アルゴリズム
8
索アルゴリズムである遺伝的アルゴリズム(Genetic Algorithm; GA)を利用すればよい [1][2][3].このアルゴリズムで
は,各探索点を個体で表現し,個体集団に対する自然淘汰および交叉,突然変異などの操作で新しい探索点を生成する
ことにより,条件を満たす解を効率良く探索する(図 3).各個体は探索点を記述する染色体を持ち,適応度と呼ばれる
評価値によって評価される.適応度が高い個体ほど次世代に生き残りやすく,適応度が低い個体は淘汰されやすい.そ
して,2 つの親個体の染色体を交叉させて子孫の染色体を作ったり,低確率で突然変異させて別の染色体を生成したりす
ることにより世代交代を行っていく.この世代交代を繰り返すことにより,個体集団の適応度が増加し,徐々に条件を満
たす解に近づいていく.以下では,より詳しい説明を行う.
3.2.1
初期個体集団の生成
まず初めに,ある規定数(たとえば 100 個)の個体から構成される初期個体集団を生成する.それぞれの個体の染色
体はランダムに設定する.
3.2.2
適応度の計算
適応度とは,個体の染色体によって示される解が,問題の条件をどの程度満たしているか(条件を満たす解にどれだ
け近いか)を表す非負の値である.適応度の計算方法は問題に応じて決定する.たとえば本コンテストの問題では,
1
(適応度)=
(問題文中の『 』内を満たす (m, n, p) の組の数)
(1)
という式で適応度を計算するのが一つの方法である.
3.2.3
ルーレットルールによる自然淘汰
自然淘汰を実現する方法として,ルーレットルールというものがある.これは,現在の個体集団から重複を許して新
たに同じ数の個体を生存個体として選択するというものである.適応度 f (Ii ) の個体 Ii が選ばれる確率 P (Ii ) は次式の
ようにする.
f (Ii )
P (Ii ) = ∑
(2)
f (Ik )
k
I1
I2
θ2
θ1
θ5
θ3
I3
I5
θ4
I4
θi ∝ f (Ii ) [f (Ii ) : 個体 Ii の適応度]
図 4: ルーレットルール
9
すなわち,各個体が選ばれる確率はその個体の適応度に比例する(図 4).
3.2.4
交叉
遺伝的アルゴリズムでは,交叉と呼ばれる操作によって新たな個体(探索点)を発生させる.交叉の方法としては様々
なものがあるが,そのうちの一つとして以下のような方法がある.まず,個体集団の一部(たとえば全体の 75%)の個
体をランダムにカップリングさせる.そして,親個体の染色体をランダムな場所で切って入れ替えることにより子孫の
染色体を生成する.たとえば,親の染色体が “ABCDBC”, “ADBCAD” で,切断点が 4 文字目の後ろに(乱数で)決定
されたとすると,子孫の染色体は “ABCDAD”, “ADBCBC” となる.
3.2.5
突然変異
探索範囲を限定しすぎないようにするため,染色体をランダムに変更する突然変異と呼ばれる操作を低確率で実行す
る.本コンテストの問題を解く場合であれば,各個体について 1% の確率でランダムな場所をランダムな文字に変更す
る(たとえば “ABDABD” → “ABDDBD”)という方法が一例として挙げられる.
3.2.6
コンテストに提出するアルゴリズムについて
コンテストに提出するアルゴリズムには特に制限を設けていないが,この遺伝的アルゴリズムによるアプローチも是
非試していただきたい.遺伝的アルゴリズムは汎用性の高い探索アルゴリズムではあるものの,探索効率の面では特定
の問題解決に特化したアルゴリズムに劣ることが多い.しかし,遺伝的アルゴリズムでも工夫次第でかなり効率的に探
索できる可能性はある.適応度の計算方法,交叉の方法,突然変異の確率,並列計算における仕事の割り振り方などを
様々に変えてみてほしい.最終的に遺伝的アルゴリズムより高速に計算できるアルゴリズムを考えた場合でも,遺伝的
アルゴリズムと比較して考察すればより良質なレポートになるだろう.
参考文献
[1] J. H. Holland: “Adaptation in Natural and Artificial Systems,” The Univ. Michigan Press, 1975.
[2] 北野 宏明: “遺伝的アルゴリズム,” 産業図書, 1993.
[3] 長尾 智晴: “進化的画像処理,” 昭晃堂, 2002.
10