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% Snapshot the USER capacities before the solver_ssa preamble rewrites
112% cap/classcap in place (it folds the state-space cutoff into them,
113% making every open class look capacity-constrained).
115userClasscap = sn.classcap;
116[probSysState,SSq,arvRates,depRates,~,~,sncl] = solver_ssa(sn, init_state, repoptions, eventCache);
125 refsf = sncl.stationToStateful(sncl.refstat(k));
126 XN(k) = probSysState*depRates(:,refsf,k);
128 isf = sncl.stationToStateful(ist);
129 TN(ist,k) = probSysState*depRates(:,isf,k);
130 QN(ist,k) = probSysState*SSq(:,(ist-1)*K+k);
131 switch sncl.sched(ist)
132 case SchedStrategy.INF
133 UN(ist,k) = QN(ist,k);
135 % we use Little's law, otherwise there are issues in
136 % estimating
the fraction of time assigned to
class k (to
138 if ~isempty(PH{ist}{k})
139 % For a USER-capacity-constrained OPEN
class the
140 % arrival-based estimator counts dropped jobs (offered
141 % load); use
the departure (carried load) estimator
for
142 % such a
class. The pre-preamble snapshot excludes
the
143 % cutoff-derived truncation caps.
144 if isinf(sncl.njobs(k)) && (isfinite(userCap(ist)) || isfinite(userClasscap(ist,k)))
145 UN(ist,k) = TN(ist,k)/rates(ist,k)/S(ist);
147 UN(ist,k) = probSysState*arvRates(:,ist,k)/rates(ist,k)/S(ist);
157 RN(ist,k) = QN(ist,k)./TN(ist,k);
162 CN(k) = NK(k)./XN(k);
172% update cache actual hit and miss data
for this replication
173TNcache = zeros(sncl.nstateful,K);
175 for isf=1:sncl.nstateful
176 if sncl.nodetype(isf) == NodeType.Cache
177 TNcache(isf,k) = probSysState*depRates(:,isf,k);
182 for isf=1:sncl.nstateful
183 if sncl.nodetype(isf) == NodeType.Cache
184 ind = sncl.statefulToNode(isf);
185 if length(sncl.nodeparam{ind}.hitclass)>=k
186 h = sncl.nodeparam{ind}.hitclass(k);
187 m = sncl.nodeparam{ind}.missclass(k);
188 sncl.nodeparam{ind}.actualhitprob(k) = TNcache(isf,h)/sum(TNcache(isf,[h,m]));
189 sncl.nodeparam{ind}.actualmissprob(k) = TNcache(isf,m)/sum(TNcache(isf,[h,m]));