Ho trovato una soluzione un po 'soddisfacente, quindi nel caso in cui qualcuno fosse interessato a condividerla. Apprezzerei ancora i commenti su come migliorare/perfezionare l'approccio.
Fondamentalmente, ho deciso che l'unico modo ragionevole è quello di costruire un (molto) modello rudimentale dello scheduler per il ciclo parallelo:
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
Il parametro cost_it
Per un'iterazione vuota è una miscela grezza di molti diversi effetti collaterali, che potrebbe essere separata: il costo di un'iterazione vuota in un for
/parfor
-loop (potrebbe anche essere diverso per blocco), nonché il tempo di avvio resp. trasmissione dei dati del parfor
-loop (e probabilmente altro). Il mio motivo principale per mettere insieme tutto è che non voglio stimare/determinare i costi più granulari.
Uso la routine sopra per determinare il cut-off nel modo seguente:
% 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
In particolare, non gonfia/cambio il valore di est_cost_para
che presuppone (a parte cost_it
) la pianificazione più ottimistica possibile. Lo lascio come è principalmente perché non so cosa funzionerebbe meglio. Per essere conservativi (cioè evitare di nutrire blocchi troppo grandi al ciclo parallelo), si potrebbe ovviamente aggiungere una certa percentuale come tampone o persino usare una potenza> 1 per gonfiare il costo parallelo.
Nota anche quello est_cost_para
viene chiamato con successive meno blocchi (anche se uso il nome variabile cost_blocks
Per entrambe le routine, uno è un sottoinsieme dell'altro).
Rispetto all'approccio nella mia vercida domanda vedo due vantaggi principali:
- La dipendenza relativamente intricata tra i dati (sia il numero di blocchi che il loro costo) e il numero di core viene catturata molto meglio con lo scheduler simulato di quanto sarebbe possibile con una singola formula.
- Calcolando il costo per tutte le possibili combinazioni di distribuzione seriale/parallela e quindi prendendo il minimo, non si può essere "bloccati" troppo presto durante la lettura nei dati da un lato (ad es. Da un salto che è grande rispetto ai dati finora, ma piccolo rispetto al totale).
Naturalmente, la complessità asintotica è più alta chiamando est_cost_para
con il suo amato tutto il tempo, ma nel mio caso (num_blocks<500
) questo è assolutamente trascurabile.
Infine, se un valore decente di cost_it
non si presenta prontamente, si può provare a calcolarlo misurando il tempo di esecuzione effettivo di ciascun blocco, nonché la parte puramente parallela di esso, e quindi cercando di adattarsi ai dati risultanti alla previsione dei costi e ottenere un valore aggiornato di cost_it
Per la prossima chiamata della routine (usando la differenza tra costo totale e costo parallelo o inserendo un costo di zero nella formula montata). Questo dovrebbe sperare di "convergere" al valore più utile di cost_it
per il problema in questione.