优化并行池以支持多线程计算
本示例演示了如何通过调整并行池中每个工作单元的线程数来提高并行计算的性能。
MATLAB ® 采用隐式多线程技术,使某些数值函数能够利用多个核,从而提高计算效率。默认情况下,MATLAB 客户端支持隐式多线程。然而,并行池工作单元默认使用单个计算线程,因为它们通常与单个内核相关联。如果您的代码中的 MATLAB 函数能从隐式多线程中获益,您可以增加每个工作单元线程上的计算线程数,以充分利用内置的并行性。
在此示例中,您将比较在并行池上执行计算与在 MATLAB 客户端上执行计算所获得的加速比。此比较可帮助您确定并行池的最佳工作单元配置。您可以按照以下步骤,为您的具体并行应用程序和硬件找到最佳配置。本示例中的执行时间是在一台搭载 Windows 11 系统、配备 Intel® Xeon® W-2133 @ 3.60 GHz 处理器的测试系统上测得的。

设置执行参数
例如,在此示例中,您将测量使用同一输入矩阵反复执行矩阵乘法操作的执行时间。
首先,确定 MATLAB 默认使用的最大计算线程数。然后,将迭代次数设置为与该最大值成正比。此示例可能需要几分钟才能完成。为了缩短执行时间,可以考虑减少迭代次数。
nT = maxNumCompThreads; numIterations = nT*10;
初始化待相乘的数组。
N = randn(5000);
在后续步骤中使用这些变量,以测量在不同执行环境下的执行时间。
比较客户端与并行池中的执行情况
测量客户端反复计算 N*N 的乘积所花费的时间。
timer = tic; for iteration = 1:numIterations outMulti = N*N; end tClient = toc(timer)
tClient = 46.1742
启动一个并行池,其工作单元数量应等于本地计算机上可用的最大计算线程数。
pool = parpool("Processes",nT);Starting parallel pool (parpool) using the 'Processes' profile ... Connected to parallel pool with 6 workers.
将 for-loop 转换为 parfor-loop,并在并行池中的所有工作单元上执行 parfor-loop。测量执行时间。
timer = tic; parfor iteration = 1:numIterations outPool = N*N; end tPool = toc(timer)
tPool = 52.5498
通过计算客户端的执行时间与并行池的执行时间之比,来计算加速倍数。将客户端执行时间除以它本身,得到速度提升比为 1,以便与池执行时间进行比较。
speedup = tClient./[tClient,tPool];
比较这些加速比。客户端的加速比与拥有六个工作单元的并行池大致相当。这一结果表明以下情况之一:
parfor-循环的并行开销很大。在具有多核的客户端上,
mtimes函数已经通过多线程实现了并行化。既要考虑较高的并行开销,又要兼顾现有多线程带来的优势。
figure; bar(speedup) xticklabels(["Client","Pool"]); xlabel("Execution Environment") ylabel("Speedup Ratio") grid on

由于并行池的加速比远低于预期,以下各节将探讨是否可以通过以下方式进一步提升性能:
计算
parfor循环代码的可扩展性,以确定是否可以通过减少工作单元数量来获得更好的性能。检查
mtimes是否已通过隐式多线程实现了并行化。增加每个工作单元的计算线程数,以提升性能。
计算可扩展性
要确定您的 parfor-loop 的可扩展性,请测量随着工作单元数量的变化,加速比如何变化。这有助于您了解您的代码是否能从额外的并行资源中获益,并找出可扩展性的任何限制。
创建一个数组来存储每次测试的结果。
tScale = zeros(1,nT);
使用 for 循环遍历不同数量的工作单元来运行 parfor 循环。要指定执行 parfor-loop 的工作单元数量,请使用 parfor 的第二个参量。
for j = 1:nT timer = tic; parfor (iteration = 1:numIterations,j) outPool = N*N; end tScale(j) = toc(timer); end
通过计算单个工作单元的执行时间与不同数量工作单元的执行时间之比,来计算加速比。
speedupScale = tScale(1)./tScale;
为了直观展示计算性能随工作单元数量的增加而提升的情况,请绘制加速比与工作单元数量的关系图。加速比随工作单元数量的增加而线性增长,但在达到四个工作单元后,开始呈现非线性增长。由于任务协调和数据传输带来的管理成本,增加超过四名工作单元将导致收益递减。
figure; plot(1:nT,speedupScale); hold on plot(1:nT,1:nT,"--"); hold off title("Speedup with Number of Workers"); xlabel("Number of workers"); xticks(1:nT); ylabel("Speedup Ratio"); legend("Measured speedup","Ideal speedup",Location="bestoutside") grid on

如果您的代码能从隐式多线程中获益,那么您可以通过增加每个工作任务的线程数来进一步提升并行池的性能。首先,测试一下您的代码是否能从多线程中获益。
多线程代码检查
MATLAB 支持对各种线性代数和数值函数进行隐式多线程计算,在满足特定条件时,这些函数可在多个核上运行。有关多线程的更多信息,请参阅在多核和多处理器计算机上运行 MATLAB。
要确定您的代码能否从隐式多线程中获益,请比较单线程客户端、多线程客户端(多线程)以及并行池的执行时间。
要获取单线程执行时间,请使用 maxNumCompThreads 函数将 MATLAB 客户端限制为单线程。
maxNumCompThreads(1); timer = tic; for iteration = 1:numIterations outSingle = N*N; end tSingle = toc(timer)
tSingle = 214.7032
通过计算单线程客户端的执行时间与先前测得的多线程客户端和并行池的执行时间之比,来计算加速倍数。将单线程客户端的执行时间除以它本身,得出加速比为 1,以便与其他执行时间进行比较。
speedupMulti = tSingle./[tSingle,tClient,tPool];
比较不同执行环境下的加速比。
figure; bar(speedupMulti) xlabel("Execution Environment") xticklabels(["Single-threaded","Multithreaded","Pool"]) ylabel("Speedup Ratio") grid on

在客户端启用隐式多线程后,矩阵乘法运算的性能有所提升,且多线程带来的性能提升与并行池带来的效果相当。为了进一步提升性能,您可以在池中的工作单元上利用 mtimes 函数的隐式多线程功能。
将客户端重置为使用默认的最大线程数。
maxNumCompThreads("automatic");在工作单元线程池上测试多线程
更改每个工作单元线程所使用的计算线程数,以便工作单元能够以多线程模式运行,并利用 mtimes 等函数的内置并行性。您可以进行实验,以找到适合您具体问题和硬件的工作单元与线程的最佳组合。
请再次确定您的计算机上可用的计算线程最大数量。目前,计算线程的最大数量等于您计算机上的物理核数。
nT = maxNumCompThreads;
该池中所有工作单元线程的总数不得超过您计算机上的最大计算线程数。请确保 NumWorkers x NumThreads ≤ 最大计算线程数。否则,您的性能可能会降低。
使用示例末尾定义的 findFactors 辅助函数,找出最大计算线程数的所有可能因数。这些因数代表了潜在的工作单元数量。通过将总线程数除以工作单元数,计算出每个工作单元的线程数。
numWorkers = findFactors(nT); numThreads = nT./numWorkers;
准备一个数组,用于存储不同工作单元和线程组合的执行时间。
tThreads = zeros(size(numWorkers));
对于每种工作单元与线程的组合,请使用 parfevalOnAll 函数来更改池中所有工作单元的线程数,并使用 parfor 的第二个参量来指定要使用的工作单元数量。如果您在集群工作单元池上运行此测试,请使用 parcluster 函数创建每个工作单元包含所需数量线程的池。有关该流程的示例,请参阅扩展至集群。
for j = 1:length(numWorkers) setNumCompThreads = parfevalOnAll(pool, ... @maxNumCompThreads,0,numThreads(j)); fetchOutputs(setNumCompThreads); timer = tic; parfor (iteration = 1:numIterations,numWorkers(j)) outPool = N*N; end tThreads(j) = toc(timer); end resetNumCompThreads = parfevalOnAll(pool,@maxNumCompThreads,0,1); fetchOutputs(resetNumCompThreads);
通过计算所有线程都在一个工作单元上的执行时间与其他工作单元-线程组合的执行时间之比,来计算每种工作单元-线程组合的加速比。
speedupThreads = tThreads(1)./tThreads;
比较加速比,以确定哪种工作单元组合能带来最佳性能。对于这个具体的问题和硬件,由两个工作单元(每个工作单元包含三个线程)组成的组合,其性能优于其他组合。
figure; bar(speedupThreads); x = compose("%d - %d",numWorkers',numThreads'); xticklabels(x); xlabel("Worker-Thread Combination"); ylabel("Speedup Ratio"); grid on

比较执行环境
通过计算多线程客户端的执行时间与以下各项执行时间的比值,来计算每个环境的加速倍数:
默认的并行池,包含六个单线程工作单元
经过优化的并行池,包含两个工作单元,每个工作单元使用三个线程。
[tOptPool,idx] = min(tThreads); optNumThreads = numThreads(idx); speedupEnvironments = tClient./[tClient,tPool,tOptPool];
可视化不同环境下的加速比(以多线程客户端的加速比为基准进行归一化),以确定哪种环境性能最佳。对于这个问题,在本地计算机上创建一个包含两个工作单元、每个工作单元包含三个线程的并行池,性能最佳。
figure; bar(speedupEnvironments); str = sprintf("Pool with %d Threads/Worker",optNumThreads); xticklabels(["Multithreaded Client","Default Pool",str]) xlabel("Execution Environment"); ylabel("Speedup Ratio"); title("Comparison of Speedup on Different Environments"); grid on

删除该资源池,为下一步做准备。
delete(pool);
扩展至集群
如果您可以访问一个远程集群,就可以在采用最优线程数的工作单元上计算 parfor-loop 的可扩展性。
在远程集群上创建一个包含 16 个工作单元的并行池。若要请求一个每个工作单元具有最优线程数的并行池,请创建一个并行集群对象并设置 NumThreads 属性。在以下代码中,请将 myCluster 替换为您远程集群配置文件的名称。
numClusterWorkers = 16;
cluster = parcluster("myCluster");
cluster.NumThreads = optNumThreads;
pool = parpool(cluster,numClusterWorkers);Starting parallel pool (parpool) using the 'myCluster' profile ... Connected to parallel pool with 16 workers.
与之前一样,在运行相同代码但使用不同数量的工作单元时,测量 parfor 循环的执行时间。要指定执行 parfor-loop 的工作单元数量,请使用 parfor 的第二个参量。
clusterScale = zeros(1,numClusterWorkers); for j = 1:numClusterWorkers timer = tic; parfor (iteration = 1:numIterations,j) outPool = N*N; end clusterScale(j) = toc(timer); end
通过计算单个工作单元的执行时间与不同数量工作单元的执行时间之比,来计算加速比。
speedupClusterScale = clusterScale(1)./clusterScale;
为了直观展示计算性能随工作单元数量的增加而提升的情况,请绘制加速比与工作单元数量的关系图。结果表明,与默认并行池相比,优化后的并行池的可扩展性得到了提升。
figure; plot(1:nT,speedupScale); hold on plot(1:numClusterWorkers,speedupClusterScale); plot(1:numClusterWorkers,1:numClusterWorkers,"--"); hold off title("Speedup with Number of Workers"); xlabel("Number of workers"); xticks(1:numClusterWorkers); ylabel("Speedup Ratio"); legend("Default pool speedup","Optimized pool speedup", ... "Ideal speedup",Location="northwest") grid on

计算完成后,请删除远程集群上的并行池。
delete(pool);
辅助函数
findFactors 函数返回指定数字的所有因数。实际上,它列出了能够均匀分配计算线程的工作单元数量。
function factors = findFactors(n) % Create an array of potential factors from 1 to n potentialFactors = 1:n; % Find index of potential factors if it divides n evenly idx = mod(n,potentialFactors) == 0; % Use logical indexing to find factors factors = potentialFactors(idx); end