並列化によるシミュレーションの高速化について

半導体加工技術の進歩によって、コンピュータのCPUの性能は向上し続けている。
2005年ころまでに、それまでのクロック周波数の向上による性能向上が頭打ちとなり、CPUメーカーは一つ製品に複数のコアを搭載する、マルチコアによる性能向上が主流になった。その結果、2011年現在、一般ユーザが使うPCですら、2〜8コアのCPUを搭載するのが普通となっている。

複数個のコアが搭載されたCPUを利用する利点は、複数のアプリケーションを同時に実行しても、それらが独立に動くことにある。1つしかコアがない場合は、複数のアプリケーションを実行する際、一定時間ごとにCPUが実行するアプリケーションを切り替える(割り込み)により処理していたため、余分に時間がかかる。それに対して、マルチコアCPUの場合は、この割り込みが生じない。シミュレータでパラメータスキャンを実行する場合においても、各コアに違うパラメータを割り振ってそれぞれ独立に計算させることで、コアの数だけ計算速度が向上が期待される。

この手法を「きまりの悪い並列化」(Embarrassingly Paralllel)、あるいはタスク並列化と呼び、複数のパラメータを計算するような状況で、計算時間の短縮に極めて有効である。

並列化を実装するには、まず、以下の機能を有するスクリプトが必要となる。

  1. スキャンするパラメータの一覧が記載されたファイルを読み込む
  2. 各パラメータから、シミュレータを起動するタスクを並列に生成する。
  3. 現在実行中のシミュレータの数を数える。

1.はプログラムの中にデータを記述しないという、構造化プログラミングの原則に則っとり、スキャンするパラメータの名称と数値を外部のファイルから与えるために必要な機能である。
2.はこのスクリプトの中核となる機能である。この機能を実装するには、「プロセスフォーク」と呼ばれるUnix系のOperating Systemの機能を利用する。プロセスフォークでは、親となるプロセスが、独立に子プロセスを生成する。この生成された子プロセスで、シミュレータをそれぞれのパラメータを計算するシミュレーションを実行するコマンドを生成・実行している。
3.は、コアの数を超えてタスクが生成された場合、割り込みによる速度低下が生じてしまうので、同時に実行するシミュレータの上限数に達した場合、他のシミュレータの実行が終了するまで、タスクの生成を待つ、という機能を実装するために必要な機能である。

このスクリプトの処理をフローチャートにすると以下の図の通りとなる。

今回、筆者が用いた計算機は、Xeonというサーバ向けブランドのCPUを2つ搭載し、物理コアを8コア、論理コアを16コア搭載するマシンである。

最大で16のシミュレータを並列で計算する能力を有しているが、OSなどの処理が割り込みに入る可能性があったり、パラメータスキャンの途中で、中止したい場合などのために、並列化されるシミュレータは15までとしている。

また、Windowsでは、Unixと同等のプロセスフォークはサポートされていないので、Perlの外部モジュールで、プロセスフォークをエミュレートさせる必要がある。さらに、Windows向けのメジャーなPerl処理系であるActivePerlでは、ひとつの親プロセスが生成することができる子プロセスの上限が64プロセスまでと制限があるため、上限に達した時点で、パラメータのリストを記述したファイルを生成し直して、スクリプトを再起動する処理を実装している。

注:下のスクリプトはファイルハンドル、ディレクトリハンドルの記法が一部、「現代的ではない」。気が向いたら書きなおす。

use strict;
use warnings;
use POSIX ":sys_wait_h";#for fork emulation

my $version = 4.0;

#main routine
#need 3 opeland parameter files.
if( @ARGV != 3 ){
    die "Parallel Simulation Scan Script:version $version \nHow to use...like...\n$0 ModelFile.ind ParameterList.csv StopTime.csv\n";# $0 is program name
}

my $model_file = $ARGV[0];#*.ind 
my $parameter_list_file=$ARGV[1];
my $stop_time_file = $ARGV[2]; chomp($stop_time_file);
my $prefix_master =$` if($model_file =~ /.ind/);
my $exe_name="fullwave.exe";
my $process_limit=15;

if (!-f $model_file || !-f $parameter_list_file || !-f $stop_time_file){
  die "No such file!\n";
}



open (my $log_file_hdl, '>', "SimulationLog.txt");
print $log_file_hdl  "Start:" . scalar localtime time() . "\n";
close ($log_file_hdl);

#Read stoptime read
my $stop_time_string;
my $time_hdl;
open $time_hdl,'<',$stop_time_file;
for (my $i=1;$i==1;$i++){
  $stop_time_string = <$time_hdl>;
  chomp($stop_time_string);
}
close($time_hdl);

open my $list_hdl,'<',$parameter_list_file;
my @lines = <$list_hdl>;
my $parameter_name_string = $lines[0];chomp($parameter_name_string);#Read Parameter Name 
close ($list_hdl);

#for (my $i=1;!eof(LIST);$i++){
my $i = 0;
foreach my $line(@lines){
  $i++;
  if ($i > 1 ){
    chomp($line);
    my $parameter_value_string = $line;
    my $generate_success=0;
    while (!$generate_success){
      if (&process_count($exe_name) < $process_limit){
        my $pid = fork;
        if (!defined $pid){
          #Rewrite parameter list 
          splice(@lines,0,$i);
          open my $list_hdl2,'>',$parameter_list_file;
          @lines = ("$parameter_name_string\n","$parameter_value_string\n",@lines);
          print $list_hdl2 @lines;
          close $list_hdl2;
          `start $0 $model_file $parameter_list_file $stop_time_file`;#cloning
          die "Original/Clone parent is dead\n";#
        }elsif($pid){ # Parent process
          print "$line stop time=$stop_time_string is simulating\n";
        }elsif(!$pid){ #Child process
          &run_simulater($parameter_name_string,$parameter_value_string,$stop_time_string);
          print "$$ exit\n";
          exit; #Kill child
        }
        $generate_success=1;
      } else {
        $generate_success=0;
        sleep 5;
      }
    }
  }
}

#terminate check
while (&process_count($exe_name) > 0){
  sleep 10;
}

open ($log_file_hdl, '>>', "SimulationLog.txt");
print $log_file_hdl  "\nStop:" . scalar localtime time() . "\n";
close ($log_file_hdl);
`shutdown -s -t 60`;

# simulation command routine (forked children)
sub run_simulater {
  (my $parameter_name_string ,my $parameter_value_string ,my $stop_time_string) = @_;
  my @parameter_name = split(/,/,$parameter_name_string);
  my @parameter_value = split(/,/,$parameter_value_string);
  my @stop_time = split(/,/,$stop_time_string);
  my $parameter_number = @parameter_name;#Number of parameters but stoptime

  foreach my $stop_time(@stop_time){
    my $prefix = $prefix_master;
    my $parameters;
    for (my $i=0 ; $i < $parameter_number ;$i++){
      $prefix .= sprintf("%s%.2f","_$parameter_name[$i]",$parameter_value[$i]);
      $parameters .= " scan_variable=$parameter_name[$i] $parameter_name[$i]=$parameter_value[$i] ";
    }
    #FDTD stop time
    $parameters .= "scan_variable=fdtd_stop_time fdtd_stop_time=$stop_time;";
    #Run command
    #print "start /min fullwave $model_file wait=0 prefix=$prefix $parameters\n";
    `start /min fullwave $model_file wait=0 prefix=$prefix $parameters`;
  }
}

#counting thread routine
sub process_count{
  (my $exe_name) = @_;
  `tasklist > process.txt`;
  open (PS, "process.txt");
  my $process_number=0;
  while (my $line = <PS>) {
    $process_number++ if ($line =~ /$exe_name/);
  }
  close (PS);
  return $process_number;
}

sub file_transaction{
  opendir(DIR,'.');
  my @files = readdir(DIR);
  closedir(DIR);
  @files = grep(/\.dat$/,@files);
  foreach my $file(@files){
    #print "$file\n";
    open(IN,$file);
    my @lines = <IN>;
    close(IN);
    open(OUT,">$file");
    foreach my $line(@lines){
      $line =~ s/^ //;
      $line =~ s/  / /g;
      $line =~ s/ $//;
      print OUT $line;
    }
    close(OUT);
  }
}

以上のようなスクリプトにより、プログラム言語に精通していないユーザでも極めて簡単、かつ高速にパラメータスキャンを行うことが可能になった。

上で述べたが、このスクリプト・プログラムは他のシミュレータでも有効である。他のシミュレータに適用するには、run_simulationという名前のサブルーチンで、シミュレータを起動するためにコマンドを生成する処理を変更すれば良いだけで、他のシミュレータへの適用は比較的容易である。