8888PORT=21212
8989PIDS=()
9090
91- # Phase 5-A orchestration: start mock_services ONCE per host before
92- # spawning the LeafNodeRank instances. mock_services is stateless and
93- # serves all colocated leaves on a fixed port (21222 = LeafNodeRank's
94- # --mock_services_port default). Without this, LeafNodeRank's outbound
95- # fanout client cannot connect, the request handler falls back to
96- # legacy folly::futures::sleep, and the Phase 6 session-mode driver
97- # never produces QPS samples (search_qps.sh fails with "Could not
98- # find QPS in loadtest output" -> divide by zero).
99- MOCK_SERVICES_PORT=21222
100- # Binary lives under src/ — the install script unpacks the source tarball
101- # alongside run.sh and ninja builds into src/build/. run.sh's main loop
102- # already does `cd "${FEEDSIM_ROOT}/src"` before invoking LeafNodeRank;
103- # from this script we use the absolute src/ path directly.
91+ # Phase 5-A orchestration: start ONE mock_services per feedsim instance
92+ # (was: ONE per host shared by all instances). Each mock_services is
93+ # tasksetted to the same CPU range as its feedsim instance, so the two
94+ # processes share L1/L2/L3 + memory bandwidth but DON'T share queue
95+ # depth with the other instance's mock_services. This eliminates a
96+ # previously-confirmed cross-instance contention point that produced
97+ # wide per-iter QPS variance under heavy outbound fanout (see Progress
98+ # Log 2026-05-13).
99+ #
100+ # Port allocation:
101+ # feedsim instance i (1..N): listens on 21212 + (i-1)
102+ # mock_services for inst i : listens on 21222 + (i-1)
103+ # (21222 stays the i=1 default for backwards compat with single-instance
104+ # manual runs and with --mock_services_port=21222 in run.sh defaults.)
105+ MOCK_SERVICES_PORT_BASE=21222
104106MOCK_SERVICES_BIN=" ${FEEDSIM_ROOT} /src/build/workloads/ranking/mock_services/mock_services"
105- MOCK_SERVICES_LOG=" ${FEEDSIM_ROOT} /mock_services.log"
106- MOCK_SERVICES_PID=" "
107+ MOCK_SERVICES_PIDS=()
107108
108109# Resolve --silesia-dir from forwarded args ($@); fall back to the default
109110# install layout (./silesia next to run.sh). mock_services requires Silesia
@@ -134,7 +135,16 @@ else
134135 SILESIA_DIR_ABS=" $SILESIA_DIR_ARG "
135136fi
136137
138+ # start_mock_services <port> <core_range> <log_path> <io_threads>
139+ # Starts ONE mock_services pinned via taskset to the given core range.
140+ # Returns PID on stdout; appends to MOCK_SERVICES_PIDS so stop_mock_services
141+ # can reap them all on EXIT.
137142function start_mock_services() {
143+ local port=" $1 "
144+ local core_range=" $2 "
145+ local log_path=" $3 "
146+ local io_threads=" $4 "
147+
138148 if [ ! -x " $MOCK_SERVICES_BIN " ]; then
139149 echo " ERROR: mock_services binary not found at $MOCK_SERVICES_BIN " >&2
140150 exit 1
@@ -143,80 +153,88 @@ function start_mock_services() {
143153 echo " ERROR: Silesia directory not found at $SILESIA_DIR_ABS (mock_services requires it)" >&2
144154 exit 1
145155 fi
146- # Size the IO worker pool to one thread per logical core so the fbthrift
147- # server can absorb the simultaneous startup-probe burst from every
148- # LeafNodeRank thread (88-176 probes on BGM/Turin) without saturating
149- # its default 6-pool sizing and timing out per-client probes.
150- local mock_io_threads
151- mock_io_threads=" $( nproc) "
152156
153157 # Latency-shaping knobs (env-overridable). Defaults match the
154158 # mock_services compiled-in defaults: 200ms cap, 0us offset, 100us
155159 # skip threshold. Tune via MOCK_LATENCY_CAP_US / MOCK_LATENCY_OFFSET_US
156160 # / MOCK_LATENCY_SKIP_THRESHOLD_US to shape the per-RPC simulated
157161 # delay -- rpc_dist.json contains very long-tail values (p99 ~8s,
158162 # max ~28s) that make each fanout block on the slowest call.
159- local mock_latency_cap_us=" ${MOCK_LATENCY_CAP_US:- 200000} "
160- local mock_latency_offset_us=" ${MOCK_LATENCY_OFFSET_US:- 0} "
161- local mock_latency_skip_threshold_us=" ${MOCK_LATENCY_SKIP_THRESHOLD_US:- 100} "
162-
163- echo " Starting mock_services on port ${MOCK_SERVICES_PORT} (silesia=${SILESIA_DIR_ABS} , io_threads=${mock_io_threads} , cap_us=${mock_latency_cap_us} , offset_us=${mock_latency_offset_us} , skip_threshold_us=${mock_latency_skip_threshold_us} )"
164- " $MOCK_SERVICES_BIN " \
165- --port=" $MOCK_SERVICES_PORT " \
166- --mock_io_threads=" $mock_io_threads " \
167- --latency_cap_us=" $mock_latency_cap_us " \
168- --latency_offset_us=" $mock_latency_offset_us " \
169- --latency_skip_threshold_us=" $mock_latency_skip_threshold_us " \
163+ local cap_us=" ${MOCK_LATENCY_CAP_US:- 200000} "
164+ local offset_us=" ${MOCK_LATENCY_OFFSET_US:- 0} "
165+ local skip_us=" ${MOCK_LATENCY_SKIP_THRESHOLD_US:- 100} "
166+
167+ echo " Starting mock_services on port ${port} (cores=${core_range} , io_threads=${io_threads} , cap_us=${cap_us} , offset_us=${offset_us} , skip_us=${skip_us} , silesia=${SILESIA_DIR_ABS} )"
168+ taskset --cpu-list " $core_range " \
169+ " $MOCK_SERVICES_BIN " \
170+ --port=" $port " \
171+ --mock_io_threads=" $io_threads " \
172+ --latency_cap_us=" $cap_us " \
173+ --latency_offset_us=" $offset_us " \
174+ --latency_skip_threshold_us=" $skip_us " \
170175 --silesia_dir=" $SILESIA_DIR_ABS " \
171- > " $MOCK_SERVICES_LOG " 2>&1 &
172- MOCK_SERVICES_PID=$!
176+ > " $log_path " 2>&1 &
177+ local pid=$!
178+ MOCK_SERVICES_PIDS+=(" $pid " )
173179
174180 # TCP-poll readiness (mirrors the LeafNodeRank wait loop in run.sh).
175181 local max_attempts=30
176182 local attempt=0
177183 while [ " $attempt " -lt " $max_attempts " ]; do
178- if (echo > /dev/tcp/localhost/" $MOCK_SERVICES_PORT " ) 2> /dev/null; then
179- echo " mock_services is ready ( port $MOCK_SERVICES_PORT accepting connections, pid=$MOCK_SERVICES_PID )"
184+ if (echo > /dev/tcp/localhost/" $port " ) 2> /dev/null; then
185+ echo " mock_services is ready on port $port ( pid=$pid )"
180186 return 0
181187 fi
182- # Bail early if the process died.
183- if ! kill -0 " $MOCK_SERVICES_PID " 2> /dev/null; then
184- echo " ERROR: mock_services died during startup. Tail of log:" >&2
185- tail -40 " $MOCK_SERVICES_LOG " >&2
188+ if ! kill -0 " $pid " 2> /dev/null; then
189+ echo " ERROR: mock_services died during startup. Tail of $log_path :" >&2
190+ tail -40 " $log_path " >&2
186191 exit 1
187192 fi
188193 attempt=$(( attempt + 1 ))
189194 sleep 1
190195 done
191- echo " ERROR: mock_services failed to become ready within ${max_attempts} s" >&2
192- tail -40 " $MOCK_SERVICES_LOG " >&2
193- kill -SIGTERM " $MOCK_SERVICES_PID " 2> /dev/null || true
196+ echo " ERROR: mock_services failed to become ready within ${max_attempts} s on port $port " >&2
197+ tail -40 " $log_path " >&2
198+ kill -SIGTERM " $pid " 2> /dev/null || true
194199 exit 1
195200}
196201
197202function stop_mock_services() {
198- if [ -n " $MOCK_SERVICES_PID " ] && kill -0 " $MOCK_SERVICES_PID " 2> /dev/null; then
199- echo " Stopping mock_services (pid=$MOCK_SERVICES_PID )"
200- kill -SIGINT " $MOCK_SERVICES_PID " 2> /dev/null || true
201- # Give it a moment to exit cleanly, then force-kill.
202- for _ in 1 2 3 4 5; do
203- kill -0 " $MOCK_SERVICES_PID " 2> /dev/null || break
204- sleep 1
203+ local pid
204+ for pid in " ${MOCK_SERVICES_PIDS[@]} " ; do
205+ if [ -n " $pid " ] && kill -0 " $pid " 2> /dev/null; then
206+ echo " Stopping mock_services (pid=$pid )"
207+ kill -SIGINT " $pid " 2> /dev/null || true
208+ fi
209+ done
210+ # Brief grace period, then force-kill any survivors.
211+ for _ in 1 2 3 4 5; do
212+ local any_alive=0
213+ for pid in " ${MOCK_SERVICES_PIDS[@]} " ; do
214+ if [ -n " $pid " ] && kill -0 " $pid " 2> /dev/null; then
215+ any_alive=1
216+ break
217+ fi
205218 done
206- kill -SIGKILL " $MOCK_SERVICES_PID " 2> /dev/null || true
207- fi
219+ [ " $any_alive " -eq 0 ] && break
220+ sleep 1
221+ done
222+ for pid in " ${MOCK_SERVICES_PIDS[@]} " ; do
223+ kill -SIGKILL " $pid " 2> /dev/null || true
224+ done
208225}
209226
210227# When LEAFNODE_USE_LEGACY_SLEEP=1, run.sh forwards --use_legacy_sleep to
211228# LeafNodeRank and the leaf takes the legacy folly::futures::sleep path
212229# instead of fanning RPCs to mock_services. Skip the mock_services side
213230# process entirely in that case so later diffs in the stack can run
214231# end-to-end integration tests without the mock_services dependency.
232+ SKIP_MOCK_SERVICES=0
215233if [ " ${LEAFNODE_USE_LEGACY_SLEEP:- 0} " = " 1" ]; then
216234 echo " Skipping mock_services startup (LEAFNODE_USE_LEGACY_SLEEP=1)"
235+ SKIP_MOCK_SERVICES=1
217236else
218237 trap stop_mock_services EXIT INT TERM
219- start_mock_services
220238fi
221239
222240function get_cpu_range() {
@@ -259,10 +277,29 @@ echo > $BREPS_LFILE
259277# shellcheck disable=SC2086
260278for i in $( seq 1 ${NUM_INSTANCES} ) ; do
261279 CORE_RANGE=" $( get_cpu_range " ${NUM_INSTANCES} " " $(( i - 1 )) " ) "
262- CMD=" IS_AUTOSCALE_RUN=${NUM_INSTANCES} taskset --cpu-list ${CORE_RANGE} ${FEEDSIM_ROOT} /run.sh -p ${PORT} -i ${NUM_ICACHE_ITERATIONS} -o feedsim_results_${FIXQPS_SUFFIX}${i} .txt $* "
280+ MOCK_PORT=$(( MOCK_SERVICES_PORT_BASE + i - 1 ))
281+ MOCK_LOG=" ${FEEDSIM_ROOT} /mock_services_${i} .log"
282+
283+ if [ " $SKIP_MOCK_SERVICES " -eq 0 ]; then
284+ # Size mock_services io threads to roughly match this instance's
285+ # CPU share (one mock thread per core in the instance's range).
286+ # awk parses the comma-and-dash core list "0-43,88-131" into a
287+ # total core count so SMT-on hosts get the SMT-doubled count.
288+ MOCK_IO_THREADS=" $( echo " $CORE_RANGE " | awk -F' ,' ' {
289+ t = 0;
290+ for (i = 1; i <= NF; i++) {
291+ n = split($i, r, "-");
292+ if (n == 2) t += (r[2] - r[1] + 1); else t += 1;
293+ }
294+ print t;
295+ }' ) "
296+ start_mock_services " $MOCK_PORT " " $CORE_RANGE " " $MOCK_LOG " " $MOCK_IO_THREADS "
297+ fi
298+
299+ CMD=" IS_AUTOSCALE_RUN=${NUM_INSTANCES} MOCK_SERVICES_PORT=${MOCK_PORT} taskset --cpu-list ${CORE_RANGE} ${FEEDSIM_ROOT} /run.sh -p ${PORT} -i ${NUM_ICACHE_ITERATIONS} -o feedsim_results_${FIXQPS_SUFFIX}${i} .txt $* "
263300 echo " $CMD " > " ${FEEDSIM_LOG_PREFIX}${i} .log"
264301 # shellcheck disable=SC2068,SC2069
265- IS_AUTOSCALE_RUN=${NUM_INSTANCES} stdbuf -i0 -o0 -e0 taskset --cpu-list " ${CORE_RANGE} " " ${FEEDSIM_ROOT} " /run.sh -p " ${PORT} " -i " ${NUM_ICACHE_ITERATIONS} " -o " feedsim_results_${FIXQPS_SUFFIX}${i} .txt" $@ 2>&1 > " ${FEEDSIM_LOG_PREFIX}${i} .log" &
302+ IS_AUTOSCALE_RUN=${NUM_INSTANCES} MOCK_SERVICES_PORT= ${MOCK_PORT} stdbuf -i0 -o0 -e0 taskset --cpu-list " ${CORE_RANGE} " " ${FEEDSIM_ROOT} " /run.sh -p " ${PORT} " -i " ${NUM_ICACHE_ITERATIONS} " -o " feedsim_results_${FIXQPS_SUFFIX}${i} .txt" $@ 2>&1 > " ${FEEDSIM_LOG_PREFIX}${i} .log" &
266303 PIDS+=(" $! " )
267304 PHY_CORE_ID=$(( PHY_CORE_ID + CORES_PER_INST))
268305 SMT_ID=$(( SMT_ID + CORES_PER_INST))
0 commit comments