1function [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,snc]=solver_ssa_analyzer_parallel(sn, init_state, laboptions)
2% [XN,UN,QN,RN,TN,CN]=SOLVER_SSA_ANALYZER_PARALLEL(SN, INIT_STATE, LABOPTIONS)
4% Worker-count-invariant parallel SSA.
6% The simulation budget
is split into a FIXED number of independent
7% replications R (laboptions.config.nreplicas). Replication r simulates
8% ceil(samples/R) events and
is seeded deterministically with (seed+r-1):
9% inside a parfor body spmdIndex resolves to 1 in solver_ssa, so the
10% effective seed
is exactly laboptions.seed+r-1, independent of which
11% worker executes the iteration. The R replications are distributed over
12% whatever workers the parallel pool provides (and run on the client if no
13% pool exists), and the per-replication estimates are averaged.
15% Because replication r always uses the same seed and sample budget no
16% matter how many workers are available, the returned averages depend only
17% on (seed, samples, R) and are invariant to the worker count. This
is the
18% key difference from the previous spmd implementation, whose result varied
19% with the pool size (it both divided the budget by, and seeded labs from,
20% the runtime number of labs).
22% Copyright (c) 2012-2026, Imperial College London
27% Fixed number of independent replications (worker-count invariant). The
28%
default lives in SolverOptions (
'SSA' case); guard here so the analyzer
29% remains usable
if called with a hand-built options
struct.
30if isfield(laboptions,
'config') && isfield(laboptions.config,
'nreplicas') ...
31 && ~isempty(laboptions.config.nreplicas)
32 R = max(1, round(laboptions.config.nreplicas));
37baseSeed = laboptions.seed;
38perRepSamples = ceil(laboptions.samples / R);
39line_debug('SSA parallel: %d replications x %d events each (requested budget %d, effective %d)', ...
40 R, perRepSamples, laboptions.samples, perRepSamples*R);
42% per-replication outputs (sliced so they can be populated inside parfor)
52 repoptions = laboptions;
53 repoptions.samples = perRepSamples;
54 repoptions.verbose = VerboseLevel.SILENT;
55 % Deterministic per-replication seed (see header). solver_ssa adds
56 % (lab_idx-1) which
is 0 inside parfor, so this seed
is used verbatim.
57 repoptions.seed = baseSeed + r - 1;
58 [XNr{r},UNr{r},QNr{r},RNr{r},TNr{r},CNr{r},sncr{r}] = ...
59 run_replica(snc, init_state, repoptions);
62% average the per-replication estimates
70% average cache actual hit/miss probabilities across replications
72 for isf=1:sn.nstateful
73 if sn.nodetype(isf) == NodeType.Cache
74 ind = sn.statefulToNode(isf);
75 sn.nodeparam{ind}.actualhitprob(k) = 0;
76 sn.nodeparam{ind}.actualmissprob(k) = 0;
79 if length(qntmp.nodeparam{ind}.hitclass)>=k
80 sn.nodeparam{ind}.actualhitprob(k) = sn.nodeparam{ind}.actualhitprob(k) + (1/R) * qntmp.nodeparam{ind}.actualhitprob(k);
81 sn.nodeparam{ind}.actualmissprob(k) = sn.nodeparam{ind}.actualmissprob(k) + (1/R) * qntmp.nodeparam{ind}.actualmissprob(k);
92function [XN,UN,QN,RN,TN,CN,sncl]=run_replica(sn, init_state, repoptions)
93% Run a single SSA replication and reduce it to per-station/
class averages.
94% Kept as a local function so the (parfor-unfriendly) nested indexing runs
95% in an ordinary function workspace.
104% eventCache is not shared across replications
105if isfield(repoptions,'config
') && isfield(repoptions.config,'eventcache
')
106 eventCache = EventCache.create(repoptions.config.eventcache, sn);
108 eventCache = EventCache.create(false, sn);
111% see _kb/06-solver-catalog.md for rationale (SSA utilization estimator)
113userClasscap = sn.classcap;
114[probSysState,SSq,arvRates,depRates,~,~,sncl] = solver_ssa(sn, init_state, repoptions, eventCache);
123 refsf = sncl.stationToStateful(sncl.refstat(k));
124 XN(k) = probSysState*depRates(:,refsf,k);
126 isf = sncl.stationToStateful(ist);
127 TN(ist,k) = probSysState*depRates(:,isf,k);
128 QN(ist,k) = probSysState*SSq(:,(ist-1)*K+k);
129 switch sncl.sched(ist)
130 case SchedStrategy.INF
131 UN(ist,k) = QN(ist,k);
133 % see _kb/06-solver-catalog.md for rationale (SSA utilization estimator)
134 if ~isempty(PH{ist}{k})
135 % see _kb/06-solver-catalog.md for rationale (SSA utilization estimator)
136 if isinf(sncl.njobs(k)) && (isfinite(userCap(ist)) || isfinite(userClasscap(ist,k)))
137 UN(ist,k) = TN(ist,k)/rates(ist,k)/S(ist);
139 UN(ist,k) = probSysState*arvRates(:,ist,k)/rates(ist,k)/S(ist);
149 RN(ist,k) = QN(ist,k)./TN(ist,k);
154 CN(k) = NK(k)./XN(k);
164% update cache actual hit and miss data for this replication
165TNcache = zeros(sncl.nstateful,K);
167 for isf=1:sncl.nstateful
168 if sncl.nodetype(isf) == NodeType.Cache
169 TNcache(isf,k) = probSysState*depRates(:,isf,k);
174 for isf=1:sncl.nstateful
175 if sncl.nodetype(isf) == NodeType.Cache
176 ind = sncl.statefulToNode(isf);
177 if length(sncl.nodeparam{ind}.hitclass)>=k
178 h = sncl.nodeparam{ind}.hitclass(k);
179 m = sncl.nodeparam{ind}.missclass(k);
180 sncl.nodeparam{ind}.actualhitprob(k) = TNcache(isf,h)/sum(TNcache(isf,[h,m]));
181 sncl.nodeparam{ind}.actualmissprob(k) = TNcache(isf,m)/sum(TNcache(isf,[h,m]));