Se me ocurrieron una solución algo satisfactoria, por lo que en caso de que alguien esté interesado, pensé en compartirla. Todavía agradecería los comentarios sobre cómo mejorar/ajustar el enfoque.
Básicamente, decidí que la única forma sensata es construir un modelo (muy) rudimentario del planificador para el bucle paralelo:
function c=est_cost_para(cost_blocks,cost_it,num_cores)
% Estimate cost of parallel computation
% Inputs:
% cost_blocks: Estimate of cost per block in arbitrary units. For
% consistency with the other code this must be in the reverse order
% that the scheduler is fed, i.e. cost should be ascending!
% cost_it: Base cost of iteration (regardless of number of entries)
% in the same units as cost_blocks.
% num_cores: Number of cores
%
% Output:
% c: Estimated cost of parallel computation
num_blocks=numel(cost_blocks);
c=zeros(num_cores,1);
i=min(num_blocks,num_cores);
c(1:i)=cost_blocks(end-i+1:end)+cost_it;
while i<num_blocks
i=i+1;
[~,i_min]=min(c); % which core finished first; is fed with next block
c(i_min)=c(i_min)+cost_blocks(end-i+1)+cost_it;
end
c=max(c);
end
El parámetro cost_it
porque una iteración vacía es una mezcla cruda de muchos efectos secundarios diferentes, que posiblemente podrían separarse: el costo de una iteración vacía en un for
/parfor
-Loop (también podría ser diferente por bloque), así como el tiempo de inicio resp. transmisión de datos del parfor
-loop (y probablemente más). Mi razón principal para unir todo es que no quiero tener que estimar/determinar los costos más granulares.
Utilizo la rutina anterior para determinar el corte de la siguiente manera:
% function i=cutoff_ser_para(cost_blocks,cost_it,num_cores)
% Determine cut-off between serial an parallel regime
% Inputs:
% cost_blocks: Estimate of cost per block in arbitrary units. For
% consistency with the other code this must be in the reverse order
% that the scheduler is fed, i.e. cost should be ascending!
% cost_it: Base cost of iteration (regardless of number of entries)
% in the same units as cost_blocks.
% num_cores: Number of cores
%
% Output:
% i: Number of blocks to be calculated serially
num_blocks=numel(cost_blocks);
cost=zeros(num_blocks+1,2);
for i=0:num_blocks
cost(i+1,1)=sum(cost_blocks(end-i+1:end))/num_cores + i*cost_it;
cost(i+1,2)=est_cost_para(cost_blocks(1:end-i),cost_it,num_cores);
end
[~,i]=min(sum(cost,2));
i=i-1;
end
En particular, no inflé/cambio el valor de est_cost_para
que asume (aparte de cost_it
) La programación más optimista posible. Lo dejo como es principalmente porque no sé qué funcionaría mejor. Para ser conservador (es decir, evite alimentar bloques demasiado grandes al bucle paralelo), uno podría agregar algún porcentaje como amortiguador o incluso usar una potencia> 1 para inflar el costo paralelo.
Tenga en cuenta también que est_cost_para
se llama con sucesivamente menos bloques (aunque uso el nombre de la variable cost_blocks
Para ambas rutinas, una es un subconjunto del otro).
En comparación con el enfoque en mi pregunta prolongada, veo dos ventajas principales:
- La dependencia relativamente intrincada entre los datos (tanto el número de bloques como su costo) y el número de núcleos se capturan mucho mejor con el planificador simulado de lo posible con una sola fórmula.
- Al calcular el costo de todas las combinaciones posibles de distribución en serie/paralela y luego tomar el mínimo, uno no puede "atascarse" demasiado temprano mientras lee en los datos de un lado (por ejemplo, por un salto que es grande en relación con los datos hasta ahora, pero pequeño en comparación con el total).
Por supuesto, la complejidad asintótica es mayor llamando est_cost_para
con su bandeja todo el tiempo, pero en mi caso (num_blocks<500
) Esto es absolutamente insignificante.
Finalmente, si un valor decente de cost_it
no se presenta fácilmente, se puede tratar de calcularlo midiendo el tiempo de ejecución real de cada bloque, así como la parte puramente paralela, y luego tratando de ajustar los datos resultantes a la predicción de costos y obtener un valor actualizado de cost_it
Para la siguiente llamada de la rutina (utilizando la diferencia entre el costo total y el costo paralelo o al insertar un costo de cero en la fórmula ajustada). Con suerte, esto debería "converger" al valor más útil de cost_it
para el problema en cuestión.