Matlab并行代码有很多匿名函数导致内存错误
Matlab parallel code with many anonymous functions leading to memory errors
我有一个代码可以解决许多不同的科学问题 inputs/parameters。我正在使用并行 for 循环遍历一系列参数,运行 遇到内存使用问题。我已尽最大努力将代表我的代码的 MWE 放在一起。
基本上,对于每个参数组合,我 运行 在几个不同的求解器选项上进行一个小循环。在我的真实代码中,这是在改变求解器容差和使用的方程(我们有一些不同的转换可以帮助调节)。每个计算实际上是针对小型 ODE 系统(3 个方程,但每个方程都相当复杂且通常很僵硬)的射击方法,具有调用 ODE 求解器的优化例程。这每次需要 seconds/minutes 到 运行,并行化开销可以忽略不计,并且加速比几乎完全与内核数量成正比。
为了解释下面的代码,从driver
开始。首先定义一些参数(MWE中的a
和f
)并将它们保存在文件中。文件名在函数之间传递。然后创建 3 组(在本例中)求解器参数,它们选择 ode 求解器、公差和要使用的方程组。然后进入 for 循环,循环遍历其他一些参数 c
,在每次迭代中使用每组求解器参数来调用优化函数。最后,我保存了一个包含每次迭代结果的临时文件(这样我就不会在服务器出现故障时丢失所有内容)。这些文件大约有 1kB,而我只有大约 10,000 个,所以整体大小在 10MB 左右。在主循环之后,我将所有内容重新组合成单个向量。
equations
函数创建要求解的实际微分方程,这是使用 switch 语句选择要求解的方程 return 来完成的。 objectiveFunction
函数使用 str2func
指定 ODE 求解器,调用 equations
获取要求解的方程,然后求解它们并计算 objective 函数值。
问题是似乎存在某种内存泄漏。一段时间后,大约几天后,代码变慢并最终给出内存不足错误(运行ning 在 48 个内核上,可用内存约为 380GB,ode15s
给出了错误)。随着时间的推移,内存使用量的增加相当缓慢,但确实存在,我无法弄清楚是什么原因造成的。
具有 10,000 个值的 MWE c
需要相当长的时间才能 运行(实际上 1,000 个可能就足够了),并且每个 worker 的内存使用量确实会随着时间的推移而增加。我认为文件 loading/saving 和作业分配会导致相当多的开销,这与我的实际代码不同,但这并不影响内存使用。
我的问题是,是什么导致内存使用量增长缓慢?
我对问题原因的看法是:
- 使用
str2func
不是很好,我应该改用 switch
并接受必须将求解器显式写入代码吗?
- 所有一直被调用的匿名函数(在 ODE 求解器中)都保留着工作区数据,而不是在每个
parfor
迭代结束时释放它
- 抑制警告导致问题:我抑制了很多 ODE 步长警告(这不应该是一个因素,因为导致问题的错误已在 2017a 中修复,我使用的服务器 运行 2017b)
fminbnd
或 ode15s
中的某些内容实际上正在泄漏内存
我无法想出一个很好和高效地绕过 1 和 2 的方法(从代码性能和代码编写的角度来看),我怀疑 3 或 4 是否真的是问题所在。
驱动函数如下:
function [xi,mfv] = driver()
% a and f are used in all cases. In actual code these are defined in a
% separate function
paramFile = 'params';
a = 4;
f = @(x) 2*x;
% this filename (params) gets passed around from function to function
save('params.mat','a','f')
% The struct setup has specifc options for the each iteration
setup(1).method = 'ode45'; % any ODE solver can be used here
setup(1).atol = 1e-3; % change the ODE solver tolerance
setup(1).eqs = 'second'; % changes what equations are solved
setup(2).method = 'ode15s';
setup(2).atol = 1e-3;
setup(2).eqs = 'second';
setup(3).method = 'ode15s';
setup(3).atol = 1e-4;
setup(3).eqs = 'first';
c = linspace(0,1);
parfor i = 1:numel(c) % loop over parameter c
xi = 0;
minFVal = inf;
for j = 1:numel(setup) % loop over each set configuration setup
% find optimal initial condition and record corresponding value of
% objective function
[xInitial,fval] = fminsearch(@(x0) objectiveFunction(x0,c(i),...
paramFile,setup(j)),1);
if fval<minFVal % keep the best solution
xi = xInitial;
minFVal = fval;
end
end
% save some variables
saveInParForLoop(['tempresult_' num2str(i)],xi,minFVal);
end
% Now combine temporary files into single vectors
xi = zeros(size(c)); mfv = xi;
for i = 1:numel(c)
S = load(['tempresult_' num2str(i) '.mat'],'xi','minFVal');
xi(i) = S.xi;
mfv(i) = S.minFVal;
end
% delete the temporary files now that the data has been consolidated
for i = 1:numel(c)
delete(['tempresult_' num2str(i) '.mat']);
end
end
function saveInParForLoop(filename,xi,minFVal)
% you can't save directly in a parfor loop, this is the workaround
save(filename,'xi','minFVal')
end
这里是定义方程的函数
function [der,transform] = equations(paramFile,setup)
% Defines the differential equation and a transformation for the solution
% used to calculate the objective function
% Note in my actual code I generate these equations earlier
% and pass them around directly, rather than always redefining them
load(paramFile,'a','f')
switch setup.eqs
case 'first'
der = @(x) f(x)*2+a;
transform = @(x) exp(x);
case 'second'
der = @(x) f(x)/2-a;
transform = @(x) sqrt(abs(x));
end
这里是计算objective函数
的函数
function val = objectiveFunction(x0,c,paramFile,setup)
load(paramFile,'a')
% specify the ODE solver and AbsTol from s
solver = str2func(setup.method);
options = odeset('AbsTol',setup.atol);
% get the differential equation and transform equations
[der,transform] = equations(paramFile,setup);
dxdt = @(t,y) der(y);
% solve the IVP
[~,y] = solver(dxdt,0:.05:1,x0,options);
% calculate the objective function value
val = norm(transform(y)-c*a);
如果您 运行 此代码将创建 100 个临时文件,然后将其删除,并且还会创建 params
文件,该文件不会被删除。您将需要并行计算工具箱。
您可能 运行 遇到这个已知问题:https://uk.mathworks.com/support/bugreports/1976165。这在刚刚发布的 R2019b 中被标记为已修复。 (由此造成的泄漏很小但持续存在 - 因此可能确实需要几天时间才能显现出来)。
我有一个代码可以解决许多不同的科学问题 inputs/parameters。我正在使用并行 for 循环遍历一系列参数,运行 遇到内存使用问题。我已尽最大努力将代表我的代码的 MWE 放在一起。
基本上,对于每个参数组合,我 运行 在几个不同的求解器选项上进行一个小循环。在我的真实代码中,这是在改变求解器容差和使用的方程(我们有一些不同的转换可以帮助调节)。每个计算实际上是针对小型 ODE 系统(3 个方程,但每个方程都相当复杂且通常很僵硬)的射击方法,具有调用 ODE 求解器的优化例程。这每次需要 seconds/minutes 到 运行,并行化开销可以忽略不计,并且加速比几乎完全与内核数量成正比。
为了解释下面的代码,从driver
开始。首先定义一些参数(MWE中的a
和f
)并将它们保存在文件中。文件名在函数之间传递。然后创建 3 组(在本例中)求解器参数,它们选择 ode 求解器、公差和要使用的方程组。然后进入 for 循环,循环遍历其他一些参数 c
,在每次迭代中使用每组求解器参数来调用优化函数。最后,我保存了一个包含每次迭代结果的临时文件(这样我就不会在服务器出现故障时丢失所有内容)。这些文件大约有 1kB,而我只有大约 10,000 个,所以整体大小在 10MB 左右。在主循环之后,我将所有内容重新组合成单个向量。
equations
函数创建要求解的实际微分方程,这是使用 switch 语句选择要求解的方程 return 来完成的。 objectiveFunction
函数使用 str2func
指定 ODE 求解器,调用 equations
获取要求解的方程,然后求解它们并计算 objective 函数值。
问题是似乎存在某种内存泄漏。一段时间后,大约几天后,代码变慢并最终给出内存不足错误(运行ning 在 48 个内核上,可用内存约为 380GB,ode15s
给出了错误)。随着时间的推移,内存使用量的增加相当缓慢,但确实存在,我无法弄清楚是什么原因造成的。
具有 10,000 个值的 MWE c
需要相当长的时间才能 运行(实际上 1,000 个可能就足够了),并且每个 worker 的内存使用量确实会随着时间的推移而增加。我认为文件 loading/saving 和作业分配会导致相当多的开销,这与我的实际代码不同,但这并不影响内存使用。
我的问题是,是什么导致内存使用量增长缓慢?
我对问题原因的看法是:
- 使用
str2func
不是很好,我应该改用switch
并接受必须将求解器显式写入代码吗? - 所有一直被调用的匿名函数(在 ODE 求解器中)都保留着工作区数据,而不是在每个
parfor
迭代结束时释放它 - 抑制警告导致问题:我抑制了很多 ODE 步长警告(这不应该是一个因素,因为导致问题的错误已在 2017a 中修复,我使用的服务器 运行 2017b)
fminbnd
或ode15s
中的某些内容实际上正在泄漏内存
我无法想出一个很好和高效地绕过 1 和 2 的方法(从代码性能和代码编写的角度来看),我怀疑 3 或 4 是否真的是问题所在。
驱动函数如下:
function [xi,mfv] = driver()
% a and f are used in all cases. In actual code these are defined in a
% separate function
paramFile = 'params';
a = 4;
f = @(x) 2*x;
% this filename (params) gets passed around from function to function
save('params.mat','a','f')
% The struct setup has specifc options for the each iteration
setup(1).method = 'ode45'; % any ODE solver can be used here
setup(1).atol = 1e-3; % change the ODE solver tolerance
setup(1).eqs = 'second'; % changes what equations are solved
setup(2).method = 'ode15s';
setup(2).atol = 1e-3;
setup(2).eqs = 'second';
setup(3).method = 'ode15s';
setup(3).atol = 1e-4;
setup(3).eqs = 'first';
c = linspace(0,1);
parfor i = 1:numel(c) % loop over parameter c
xi = 0;
minFVal = inf;
for j = 1:numel(setup) % loop over each set configuration setup
% find optimal initial condition and record corresponding value of
% objective function
[xInitial,fval] = fminsearch(@(x0) objectiveFunction(x0,c(i),...
paramFile,setup(j)),1);
if fval<minFVal % keep the best solution
xi = xInitial;
minFVal = fval;
end
end
% save some variables
saveInParForLoop(['tempresult_' num2str(i)],xi,minFVal);
end
% Now combine temporary files into single vectors
xi = zeros(size(c)); mfv = xi;
for i = 1:numel(c)
S = load(['tempresult_' num2str(i) '.mat'],'xi','minFVal');
xi(i) = S.xi;
mfv(i) = S.minFVal;
end
% delete the temporary files now that the data has been consolidated
for i = 1:numel(c)
delete(['tempresult_' num2str(i) '.mat']);
end
end
function saveInParForLoop(filename,xi,minFVal)
% you can't save directly in a parfor loop, this is the workaround
save(filename,'xi','minFVal')
end
这里是定义方程的函数
function [der,transform] = equations(paramFile,setup)
% Defines the differential equation and a transformation for the solution
% used to calculate the objective function
% Note in my actual code I generate these equations earlier
% and pass them around directly, rather than always redefining them
load(paramFile,'a','f')
switch setup.eqs
case 'first'
der = @(x) f(x)*2+a;
transform = @(x) exp(x);
case 'second'
der = @(x) f(x)/2-a;
transform = @(x) sqrt(abs(x));
end
这里是计算objective函数
的函数function val = objectiveFunction(x0,c,paramFile,setup)
load(paramFile,'a')
% specify the ODE solver and AbsTol from s
solver = str2func(setup.method);
options = odeset('AbsTol',setup.atol);
% get the differential equation and transform equations
[der,transform] = equations(paramFile,setup);
dxdt = @(t,y) der(y);
% solve the IVP
[~,y] = solver(dxdt,0:.05:1,x0,options);
% calculate the objective function value
val = norm(transform(y)-c*a);
如果您 运行 此代码将创建 100 个临时文件,然后将其删除,并且还会创建 params
文件,该文件不会被删除。您将需要并行计算工具箱。
您可能 运行 遇到这个已知问题:https://uk.mathworks.com/support/bugreports/1976165。这在刚刚发布的 R2019b 中被标记为已修复。 (由此造成的泄漏很小但持续存在 - 因此可能确实需要几天时间才能显现出来)。