Main Content

並列処理におけるメイクスパンの最小化

この例では、並列で処理される一連のタスクを扱います。各タスクの処理時間はわかっています。メイクスパンは、すべてのタスクを処理するのにかかる時間です。以下の図は 2 つのプロセッサを表しています。色付きの各ボックスの高さは、タスクを処理する時間の長さを表します。各タスクの実行時間はプロセッサごとに異なる場合があります。

ゴールは、メイクスパンが最小になるよう、プロセッサのタスクをスケジュールすることです。

問題の設定

この例では、11 個のプロセッサと 40 件のタスクがあります。各プロセッサが各タスクを処理する時間は、配列 processingTime で指定します。この例では、ランダムな処理時間を生成します。

rng default % for reproducibility
numberOfProcessors = 11;
numberOfTasks = 40;
processingTime = [10;7;2;5;3;4;7;6;4;3;1] .* rand(numberOfProcessors,numberOfTasks);

processingTime(i,j) は、プロセッサ i がタスク j を処理するのにかかる時間を表します。

0-1 整数計画法を使用して問題を解くために、バイナリ最適化変数配列として process を作成します。ここで、process(i,j) = 1 は、プロセッサ i がタスク j を処理することを意味します。

process = optimvar('process',size(processingTime),'Type','integer','LowerBound',0,'UpperBound',1);

各タスクは 1 つのプロセッサだけに割り当てなければなりません。

assigneachtask = sum(process,1) == 1;

目的を表すため、非負の最適化変数を makespan という名前で定義します。

makespan = optimvar('makespan','LowerBound',0);

各プロセッサがタスクを処理するために必要な時間を計算します。

computetime = sum(process.*processingTime,2);

計算した時間をメイクスパンに関連付けます。メイクスパンは、計算した各時間以上になります。

makespanbound = makespan >= computetime;

メイクスパンの最小化を目的とする最適化問題を作成し、問題の制約を含めます。

scheduleproblem = optimproblem('Objective',makespan);
scheduleproblem.Constraints.assigneachtask = assigneachtask;
scheduleproblem.Constraints.makespanbound = makespanbound;

問題を解いて解を表示する

通常の表示を行わずに問題を解きます。

options = optimoptions(scheduleproblem,'Display',"off");
[sol,fval,exitflag] = solve(scheduleproblem,'Options',options)
sol = struct with fields:
    makespan: 1.3634
     process: [11x40 double]

fval = 1.3634
exitflag = 
    OptimalSolution

返された exitflag は、ソルバーによって最適解が見つかったことを示しています。つまり、返された解は最小のメイクスパンです。

返されたメイクスパンは 1.3634 です。これは効率的なスケジュールでしょうか。確認するために、得られたスケジュールを積み重ね棒グラフとして表示します。まず、スケジュールの行列を作成します。行 i は、プロセッサ i が処理したタスクを表します。次に、スケジュールの各エントリの処理時間を求めます。

processval = round(sol.process);
maxlen = max(sum(processval,2)); % Required width of the matrix
% Now calculate the schedule matrix
optimalSchedule = zeros(numberOfProcessors,maxlen);
ptime = optimalSchedule;
for i = 1:numberOfProcessors
    schedi = find(processval(i,:));
    optimalSchedule(i,1:length(schedi)) = schedi;
    ptime(i,1:length(schedi)) = processingTime(i,schedi);
end
optimalSchedule
optimalSchedule = 11×10

    25    38     0     0     0     0     0     0     0     0
     4    12    23    32     0     0     0     0     0     0
     7    13    14    18    31    37     0     0     0     0
    35     0     0     0     0     0     0     0     0     0
     6    22    39     0     0     0     0     0     0     0
    10    26    28    30     0     0     0     0     0     0
    20     0     0     0     0     0     0     0     0     0
    21    24    27     0     0     0     0     0     0     0
     8    16    33     0     0     0     0     0     0     0
     3    17    34     0     0     0     0     0     0     0
      ⋮

スケジュールの行列を積み重ね棒グラフとして表示します。それぞれの棒の上部にタスク番号のラベルを付けます。

figure
bar(ptime,'stacked')
xlabel('Processor Number')
ylabel('Processing Time')
title('Task Assignments to Processors')
for i=1:size(optimalSchedule,1)
    for j=1:size(optimalSchedule,2)
        if optimalSchedule(i,j) > 0
            processText = num2str(optimalSchedule(i,j),"%d");
            hText = text(i,sum(ptime(i,1:j),2),processText);
            set(hText,"VerticalAlignment","top","HorizontalAlignment","center","FontSize",10,"Color","w");
        end
    end
end

積み重ね棒のうち、高さが最小のものを見つけます。これは、プロセッサが最も早く処理を停止することを表します。次に、高さが最大のものに対応するプロセッサを見つけます。

minlength = min(sum(ptime,2))
minlength = 1.0652
[~,processormaxlength] = max(sum(ptime,2))
processormaxlength = 7

minlength = 1.0652 の時点までは、すべてのプロセッサが処理中です。積み重ね棒グラフから、この時点でプロセッサ 8 の処理が停止することがわかります。processormaxlength = 7 のプロセッサが最後に処理を停止するプロセッサであり、停止するのは makespan = 1.3634 の時点です。

参考

関連するトピック