view paper/taskmanager.tex @ 3:ea6802db8b12

3章と4章もう少し
author gongo@gendarme.cr.ie.u-ryukyu.ac.jp
date Wed, 04 Feb 2009 17:46:51 +0900
parents 10f410903952
children 059572b27b8f
line wrap: on
line source

\chapter{Task Manager} \label{chapter:taskmanager}

Task Manager は、Task と呼ばれる、分割された各プログラムを管理する。
Task の単位はサブルーチンまたは関数とし、Task 同士の依存関係を考慮しながら
実行していく。

現在実装されている TaskManager の API を \tabref{taskmanager_api} に示す。

\begin{table}[htb]
  \caption{Task Manager API} \label{tab:taskmanager_api}
  \hbox to\hsize{\hfil
  \begin{tabular}{c|l} \hline \hline
    create\_task  & Task を生成する \\ \hline
    run           & 実行 Task Queue の実行 \\ \hline
    allocate      & 環境のアライメントを考慮した allocater \\ \hline
    \hline
    add\_inData   & Task への入力データのアドレスを追加 \\ \hline
    add\_outData  & Task からのデータ出力先アドレスを追加 \\ \hline
    add\_param    & Task のパラメータ (32 bits) \\ \hline
    wait\_for     & Task の依存関係の考慮 \\\hline
    set\_cpu      & Task を実行する CPU の設定 \\ \hline
    set\_post     & Task が終了したら PPE 側で実行される関数の登録 \\ \hline
    spawn         & Task を実行 Task Queue に登録する \\ \hline
  \end{tabular}\hfil}
\end{table}

以下に Task Manager を使った記述例を示す。
このプログラムは、PPE にある文字列データを、SPE が受け取って
データを改変し、PPEに戻すというプログラムである。
各 API の詳細は後述する。

\begin{verbatim}
#define STRSIZE 256
char sendStr[STRSIZE] = "Hello, World";

int
main(void)
{
    TaskManager *manager;
    HTask *task;
    int length = sizeof(char)*STRSIZE;

    // CPU_NUM: 使用する CPU の数
    manager = new TaskManager(CPUNUM);

    // TASK_RUN = TaskRun::run に対応する ID
    task = create_task(TASK_RUN);

    /**
     * タスクの入出力データの設定
     * @param[1]: address of data
     * @param[2]: size of data
     */
    task->add_inData(sendStr,length);
    task->add_outData(sendStr,length);
    
    task->spawn();
    manager->run();

    return 0;
}

TaskRun::run(void *rbuf, void *wbuf)
{
    // add_inData で指定したアドレスのデータを取得
    // recvStr = "Hello, World"
    char *recvStr = (char*)get_input(rbuf, 0);

    // fixStr にあるデータが、
    // Task 終了後、add_outData で指定した
    // sendStr に書き込まれる
    char *fixStr = (char*)get_input(wbuf, 0);

    strcpy(fixStr, recvStr);
    fixStr[0] = 'D'; // 文字列データを変換
    fixStr[3] = 'E'; // 
}

// 実行結果
before: Hello, World
after: DelEo, World

\end{verbatim}


ここからは、Task Manager の実装や API の詳細を述べる。

\section{TaskManager クラス}

TaskManager API を使用するには、まず TaskManager オブジェクトを生成する。

\begin{verbatim}
TaskManager *manager = new Taskmanager(CPU_NUM);
\end{verbatim}

CPU\_NUM は、このプログラムで使用する CPU の数になる。
生成した manager からは以下の API が使用可能になる。

\begin{itemize}
  \item create\_task(int TASK\_ID);
  \item allocate(int size);
  \item run(void);
\end{itemize}

create\_task は、指定した ID に対応する Task オブジェクト
(\ref{sec:tm_task} 節) を生成する (\ref{sec:tm_task_create} 節) 。

allocate は、動作環境のアライメントを考慮したアロケートを行う。
Cell の DMA で転送するデータのアライメントは 16 byte が基本で、
ユーザはそれをできるだけ意識しないように memory allocate が行える。

create\_task で生成したタスクは、spawn() されることにより
ActiveQueue に格納される。run は、その ActiveQueue にあるタスクを
SPE に送信することを命令する関数である。
ActiveQueue が空になったら、run を抜ける。


\section{Task (メインスレッド)}
メインスレッドでは、主にタスクの起動と、タスクに渡すオプションの設定を行う。
ここでは、タスクの情報オブジェクトとなる Task の定義と、
Task のオプション設定について説明する。

\subsection{Task の定義} \label{sec:tm_task}

以下は実行されるタスクの情報となるデータ構造である。
このデータは PPE、SPE の両方で使用される。

\begin{verbatim}
#define MAX_PARAMS 8
#define MAX_LIST_DMA_SIZE 8

class Task {
public:
    int command;
    ListDataPtr inData;
    ListDataPtr outData;
    uint32 self;

    int param_size;
    int param[MAX_PARAMS];
};

class ListElement {
public:
    int size;
    unsigned int addr;
};

class ListData {
public:
    int length;
    int size;
    int bound[MAX_LIST_DMA_SIZE];
    ListElement element[MAX_LIST_DMA_SIZE];
};
\end{verbatim}

Task クラスは、各 CPU が実行するタスクの単位オブジェクトである。
以下は、SPE が Task を参照し実行するステップである (\figref{tm_task_struct}) 。

\begin{enumerate}
\item inListData にあるアドレス(メインメモリ空間)を参照し、
  DMA でメインメモリからデータを取得する
\item command を見て、どのタスク(関数)を実行するか決定する
\item DMA read した値をタスクに渡し、実行する
\item 演算結果の DMA 転送先を、outListDataを参照して決定する
\item DMA write する
\end{enumerate}

\begin{figure}[htb]
  \begin{center}
    \includegraphics[scale=0.7]{./images/tm_task_struct.pdf}
  \end{center}
  \caption{Task の構造と、SPE での参照の仕方}
  \label{fig:tm_task_struct}
\end{figure}

複数のタスクが以上の処理を行うとき、パイプラインに沿って動作する
(\ref{sec:tm_scheduler} 節) 。

Task クラスの他に、PPE でのみ管理される HTask クラスがある。

\begin{verbatim}
class HTask : public Task {
public:
    TaskQueuePtr wait_me;  // List of task waiting for me
    TaskQueuePtr wait_i;   // List of task for which I am waiting
    void (*post_func)(void *);
    void *post_arg;
    CPU_TYPE cpu_type;
};
\end{verbatim}

HTask クラスは、Task クラスを継承したもので、wait\_me、wait\_i は
タスク依存の条件に (\ref{sec:tm_depend} 節) 、
cpu\_type はタスクが実行される CPU の切り替えに (\ref{sec:tm_cpu} 節) 、
post\_func と post\_arg は、タスク終了時に PPE で実行される
関数と引数 (\ref{sec:tm_post} 節) に用いられる。

以上のことからわかるように、Task そのものにはコードの記述はなく、
SPE にロードしてあるタスクの配列から、command に沿って取得し、
実行するだけである。したがって、
予めコード部分を SPE にロードしておく必要がある。

\subsection{Task の生成} \label{sec:tm_task_create}

Task を生成するには、TaskManager の API である create\_task() を実行する。

\begin{verbatim}
HTask* TaskManager::create_task(TASK_ID);

// 記述例
HTask *task = manager->create_task(ID);
\end{verbatim}

このタスクに渡す入力として、add\_inData と add\_param がある。

\verb+add_inData(addr, size)+ は、タスクに渡すデータのアドレスと、
そのデータのサイズを引数として入力する。
このデータは DMA 転送されるため、addr は 16 バイトアライメントが取れており、
size は 16 バイト倍数である必要がある。

\verb+add_param(param)+ は、タスクに 32bit のデータを渡す。
add\_inData で渡すには小さいものを送るのに適している。
param は アドレスとしてではなく、値を Task オブジェクトが直接持っているので、
DMA 転送は行わない。

タスクの出力先は add\_outData を使用する。
使用方法は add\_inData と同じで、アライメント、
バイト数にも気をつける必要がある。

\subsection{Task の依存関係} \label{sec:tm_depend}

TaskManager はタスク依存を解決する機能を持っている。以下は記述例である。

\begin{verbatim}
// task2 は task1 が終了してから開始する
task2->wait_for(task1);
\end{verbatim}

このとき、task1 は ActiveQueue へ、task2 は WaitQueue へ格納される。
各 CPU は、タスクが終了したらメインスレッド(PPE) へタスク終了のコマンドを
発行する。メインスレッドはそれを受け取り、WaitQueue のタスクを調べ、
タスク依存を満たしたタスクを ActiveQueue に移し替える。

wait\_for は複数のタスクを指定でき、以下の場合は、
task3 が task1、task2 の二つのタスク終了を待つ形となる。

\begin{verbatim}
task3->wait_for(task1);
task3->wait_for(task2);
\end{verbatim}

\subsection{Task を実行させる CPU の選択} \label{sec:tm_cpu}

TaskManager の set\_cpu() により、タスクをどの CPU で
実行するか選択することが出来る。

\begin{verbatim}
// SPE 1 で実行する
task1->set_cpu(SPE_1);

// SPE のどれかで実行する 
task2->set_cpu(SPE_ANY);

// PPE で実行する
task3->set_cpu(PPE);
\end{verbatim}

これにより、メインスレッド内でもタスクを実行することが可能になるため、
環境依存によるプログラム変換はタスクの部分だけとなる。よって、
\figref{manycore_step} の段階(3) と 段階 (4) のタスク単位での
相互変換が容易になる。

\subsection{Task 終了時に実行される関数} \label{sec:tm_post}

タスクが終了した際、メインスレッドで実行される関数と、その引数を指定できる。

\begin{verbatim}
int data = 3;
task->set_post(func1, (void*)data);

void
func1(void *data)
{
    printf("func1: data = %d\n", (int)data);
}

// 実行出力結果
func1: data = 3
\end{verbatim}

set\_post により、ユーザ側でもタスクが終了したということを検知できる。

また、大量のタスクに依存関係を設定する際、一度に create\_task で生成し
wait\_for で繋げるというのは難しい。
その場合、ある一定数のタスクだけ生成しておき、set\_post を使って
終了したことを確認して、その中で新たにタスクを生成していく方が望ましい
(\figref{tm_task_post}) 。

\begin{figure}[htb]
  \begin{center}
    \includegraphics[scale=0.7]{./images/tm_task_post.pdf}
  \end{center}
  \caption{Task set\_post}
  \label{fig:tm_task_post}
\end{figure}

\figref{tm_task_post} では、Func0 で task1 から task50 を生成し、
それらに set\_post(Func1) を実行する。
Func1() で、task1 から task50 までが終了したことを確認したら、
今度は task51 から task70 まで生成し、set\_post(Func2) を設定する。
このように、set\_post と wait\_for を組み合わせることで、
複雑で大量なタスクの依存関係も設定することが可能となっている。


\section{Task (CPUスレッド)}
各 CPU では、メインスレッドで生成された Task を受け取り、
その情報を元に、タスクを実行する。
ここでは、Task を扱うスケジューラの実装と、
タスクの本体となる部分の記述について説明する。

\subsection{スケジューラ} \label{sec:tm_scheduler}

生成された Task にしたがって、実際のタスクを実行するのがスケジューラである。
スケジューラは、メインスレッドで生成された TaskList を受け取り、
その中にある Task をパイプラインに沿って実行していく (\figref{tm_scheduler}) 。

\begin{figure}[htb]
  \begin{center}
    \includegraphics[scale=0.8]{images/tm_scheduler.pdf}
    \caption{スケジューラ}
    \label{fig:tm_scheduler}
  \end{center}
\end{figure}

以下がパイプラインの記述である。

\begin{verbatim}
SchedTaskBase *task1, *task2, *task3;

do {
    task3->write();
    task2->exec();
    task1->read();

    taskTmp = task3;
    task3 = task2;
    task2 = task1;
    task1 = task1->next(taskTmp);
} while (task1);
\end{verbatim}

このパイプラインでは、実際に三つの task が同時に動作しているわけではない。
read() や write() での DMA 待ちの間に、exec() を行うことで
DMA の待ち時間を隠蔽することを目的としている。

TaskList にある Task が全て終了し、メインスレッドから終了のメッセージを
受け取ったら、 task1 は NULL となり、while 文を抜ける。

SchedTaskBase クラスは、スケジューラによって実行されるインターフェースである。
SchedTaskBase を継承したクラスは以下のようなものがある。

\begin{itemize}
\item SchedMail \\
  メインスレッドからのメッセージ (Mailbox) を取得する
\item SchedTaskList \\
  TaskList を取得する
\item SchedTask \\
  TasKList から取得した Task を実行する
\item SchedExit \\
  SPE の実行を終了する
\item SchedNop \\
  何も行わない
\end{itemize}

\subsection{タスクの記述} \label{sec:stask_code}

ユーザがタスクを記述す場合、SchedTask を継承し、
パイプラインの exec 内で呼ばれている run() にタスクの処理を記述する。

\begin{verbatim}
// Hello.h
#include "SchedTask.h"

class Hello : public SchedTask {
public:
    // read,exec,write は SchedTask で記述されている
    int run(void *r, void *w);
};

// Hello.cc
#include <stdio.h>
#include "Hello.h"

int
Hello::run(void *rbuf, void *wbuf)
{
    int task_id = smanager->get_param(0);

    printf("[%d] Hello, World!!\n", task_id);

    return 0;
}
\end{verbatim}

\subsection{STaskManager}

上記の smanager とは、CPU 内で Task に関する処理を行う際に使用する
オブジェクトで、SchedTask を継承していれば、自動的に使えるようになる。

\begin{verbatim}
STaskManager *smanager;
\end{verbatim}

Task に関する処理の場合、メインスレッドの TaskManager と
同じようなアクセスが出来る方がユーザに理解しやすいと考え、このように実装した。

\tabref{sm_api} に STaskManager の API を示す。

\begin{table}[htb]
  \caption{STaskManager API} \label{tab:sm_api}
  \hbox to\hsize{\hfil
  \begin{tabular}{c|l} \hline \hline
    create\_task  & Task を生成する \\ \hline
    run           & 実行 Task Queue の実行 \\ \hline
    allocate      & 環境のアライメントを考慮した allocater \\ \hline
    \hline
    get\_input    & add\_inData で指定したデータを取得する \\ \hline
    get\_output   & add\_outData で指定した領域に書き込むバッファを取得する \\ \hline
    get\_param    & add\_param で指定した 32 bit データを取得する \\ \hline
  \end{tabular}\hfil}
\end{table}

\subsection{Input Data の取得}

\ref{sec:stask_code} 節の コードの rbuf には、
メインスレッドで \verb+add_inData()+ により指定したデータの実体が入っている
(\figref{tm_sm_rbuf}) 。

\begin{figure}[htb]
  \begin{center}
    \includegraphics[scale=0.8]{images/tm_sm_rbuf.pdf}
    \caption{add\_inData による rbuf の構造}
    \label{fig:tm_sm_rbuf}
  \end{center}
\end{figure}

このデータを扱う場合、直接 rbuf を見るのではなく

\begin{verbatim}
int *data = (int*)smanager->get_input(id);
\end{verbatim}

というようにして取得する。id は、add\_inData で指定した順番 (0〜N-1) になる。
この場合、\verb+smanager->get_input(1)+ とすれば、data2 が得られる。

同様に、\verb+add_param()+ で指定した値は \verb+get_param(id)+ で得られる。
ここの id も、\verb+get_input+ と同じく、設定した順番によって指定する。

\subsection{Output Data の扱い}

タスクが出力を行う場合、タスク生成時に \verb+add_outData+ を行い、
出力先のアドレスと、データサイズが指定されている。
wbuf は、rbuf 同様、指定したサイズ分のバッファである。
しかし、wbuf はこの時点ではただのバッファで、中には何も入っていない(不定)。
タスク終了時、スケジューラの write() で、
wbuf の値を \verb+add_outData+ で指定したアドレスに DMA 転送する
(\figref{tm_sm_wbuf}) 。

\begin{figure}[htb]
  \begin{center}
    \includegraphics[scale=0.8]{images/tm_sm_wbuf.pdf}
    \caption{add\_onData による rbuf の構造}
    \label{fig:tm_sm_wbuf}
  \end{center}
\end{figure}

wbuf から対応するバッファを取得するには以下の様に記述する。

\begin{verbatim}
int *out = (int*)smanager->get_output(id);

for (int i = 0 ; i < size; i++) {
    out[i] = i;
}
\end{verbatim}

タスク終了後、out に該当するメインメモリの領域が
out と同じ値になっている。

\subsection{CPU スレッドでのタスク生成}

各 CPU 内でも、タスクを生成することはできる。

\begin{verbatim}
Task *task = smanager->create_task(TASK_ID);
\end{verbatim}

現在の実装でメインスレッドと違う点は、
依存関係が設定できないことと、実行する CPU を選択できない(現在のCPUで実行する)
、set\_post が使用できないことである。
基本的に、タスクの生成はメインスレッドで行うが、
状況によっては CPU 内でタスクを再起動したいということがある。
例えば、タスクのある地点で DMA 待ちを行う必要が出て来た場合、
その間他の処理する部分がなければ、一旦このタスクを終了し、後続のタスクを
実行させてから再びこのタスクを実行するという使い方ができる。

\section{メインスレッドと各 CPU スレッド間との同期}

メインスレッドと各 CPU 間では、Mailbox (\ref{sec:cell_mailbox}節) を用いた
32ビットメッセージの交換により同期を行っている。
通常、スレッド間で待ち合わせを行うと処理が止まってしまい、
並列度が下がってしまうがことがあるが、Mailbox は
メッセージ交換なので待ち合わせを避けることが可能である。

\figref{tm_sync} は、PPE-SPE 間のメッセージのやりとりを表している。
メインスレッドでは、各 SPE の起動と終了、そしてタスクの管理を行っている。
そして Outbound Mailbox (SPE $\rightarrow$ PPE メッセージ) を見て、
その内容に対応する処理を PPE 上で行う。
対処した結果を Inbound Mailbox (PPE $\rightarrow$ SPE メッセージ) で伝え、
受け取った SPE はタスクを再実行する。

\begin{figure}[htb]
  \begin{center}
    \includegraphics[scale=0.8]{images/tm_sync.pdf}
    \caption{PPE, SPE threads}
    \label{fig:tm_sync}
  \end{center}
\end{figure}


\section{学生による TaskManager を用いた開発}