LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_ssa_analyzer_parallel.m
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)
3%
4% Worker-count-invariant parallel SSA.
5%
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.
14%
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).
21
22% Copyright (c) 2012-2026, Imperial College London
23% All rights reserved.
24
25snc = sn;
26
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));
33else
34 R = 8;
35end
36
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);
41
42% per-replication outputs (sliced so they can be populated inside parfor)
43XNr = cell(1,R);
44UNr = cell(1,R);
45QNr = cell(1,R);
46RNr = cell(1,R);
47TNr = cell(1,R);
48CNr = cell(1,R);
49sncr = cell(1,R);
50
51parfor r = 1:R
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);
60end
61
62% average the per-replication estimates
63QN = cellsum(QNr)/R;
64UN = cellsum(UNr)/R;
65RN = cellsum(RNr)/R;
66TN = cellsum(TNr)/R;
67CN = cellsum(CNr)/R;
68XN = cellsum(XNr)/R;
69
70% average cache actual hit/miss probabilities across replications
71for k=1:snc.nclasses
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;
77 for l=1:R
78 qntmp = sncr{l};
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);
82 end
83 end
84 end
85 end
86end
87snc = sn;
88tranSysState=[];
89tranSync=[];
90end
91
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.
96
97M = sn.nstations;
98K = sn.nclasses;
99PH = sn.proc;
100S = sn.nservers;
101NK = sn.njobs';
102rates = sn.rates;
103
104% eventCache is not shared across replications
105if isfield(repoptions,'config') && isfield(repoptions.config,'eventcache')
106 eventCache = EventCache.create(repoptions.config.eventcache, sn);
107else
108 eventCache = EventCache.create(false, sn);
109end
110
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).
114userCap = sn.cap;
115userClasscap = sn.classcap;
116[probSysState,SSq,arvRates,depRates,~,~,sncl] = solver_ssa(sn, init_state, repoptions, eventCache);
117
118XN = NaN*zeros(1,K);
119UN = NaN*zeros(M,K);
120QN = NaN*zeros(M,K);
121RN = NaN*zeros(M,K);
122TN = NaN*zeros(M,K);
123CN = NaN*zeros(1,K);
124for k=1:K
125 refsf = sncl.stationToStateful(sncl.refstat(k));
126 XN(k) = probSysState*depRates(:,refsf,k);
127 for ist=1:M
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);
134 otherwise
135 % we use Little's law, otherwise there are issues in
136 % estimating the fraction of time assigned to class k (to
137 % recheck)
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);
146 else
147 UN(ist,k) = probSysState*arvRates(:,ist,k)/rates(ist,k)/S(ist);
148 end
149 end
150 end
151 end
152end
153
154for k=1:K
155 for ist=1:M
156 if TN(ist,k)>0
157 RN(ist,k) = QN(ist,k)./TN(ist,k);
158 else
159 RN(ist,k) = 0;
160 end
161 end
162 CN(k) = NK(k)./XN(k);
163end
164
165QN(isnan(QN))=0;
166CN(isnan(CN))=0;
167RN(isnan(RN))=0;
168UN(isnan(UN))=0;
169XN(isnan(XN))=0;
170TN(isnan(TN))=0;
171
172% update cache actual hit and miss data for this replication
173TNcache = zeros(sncl.nstateful,K);
174for k=1:K
175 for isf=1:sncl.nstateful
176 if sncl.nodetype(isf) == NodeType.Cache
177 TNcache(isf,k) = probSysState*depRates(:,isf,k);
178 end
179 end
180end
181for k=1: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]));
190 end
191 end
192 end
193end
194end
Definition Station.m:245