Skip to content

Commit baf70d6

Browse files
excelle08meta-codesync[bot]
authored andcommitted
Run one mock_services per feedsim instance, taskset-isolated (#714)
Summary: Pull Request resolved: #714 Prior: run-feedsim-multi.sh started ONE mock_services on port 21222 that all colocated LeafNodeRank instances shared. Two side effects: 1. All outbound RPC fanout from every feedsim instance funnels to one mock_services process, putting all instances on a shared contention queue. 2. mock_services threads ran without CPU affinity, competing freely with the tasksetted feedsim instances for CPU scheduler decisions. Now: one mock_services per feedsim instance: - port = MOCK_SERVICES_PORT_BASE + (instance_index - 1) so instance i talks to its own mock on 21222 + (i-1). - tasksetted to the SAME CPU range as its feedsim instance, so the two processes share L1/L2/L3 + memory bandwidth but DON'T share queue depth with the other instance's mock_services. - mock_io_threads = cores_per_instance (computed from CORE_RANGE) instead of full nproc, sized to its instance's CPU share. run.sh threads MOCK_SERVICES_PORT through to LeafNodeRank's --mock_services_port. Defaults to 21222 for back-compat with single-instance manual runs and the existing per-instance default. Trade-off: 2x mock_services memory (Silesia corpus loaded twice on a 2-instance host). On BGM 251GB this is negligible. The benefit is cross-instance interference isolation — eliminates one of the shared-resource hypotheses for the per-iter QPS imbalance observed in the t4 sweep. Reviewed By: YifanYuan3 Differential Revision: D105659810
1 parent cc93beb commit baf70d6

2 files changed

Lines changed: 99 additions & 59 deletions

File tree

packages/feedsim/run-feedsim-multi.sh

Lines changed: 92 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -88,22 +88,23 @@ done
8888
PORT=21212
8989
PIDS=()
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
104106
MOCK_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"
135136
fi
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.
137142
function 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

197202
function 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
215233
if [ "${LEAFNODE_USE_LEGACY_SLEEP:-0}" = "1" ]; then
216234
echo "Skipping mock_services startup (LEAFNODE_USE_LEGACY_SLEEP=1)"
235+
SKIP_MOCK_SERVICES=1
217236
else
218237
trap stop_mock_services EXIT INT TERM
219-
start_mock_services
220238
fi
221239

222240
function get_cpu_range() {
@@ -259,10 +277,29 @@ echo > $BREPS_LFILE
259277
# shellcheck disable=SC2086
260278
for 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))

packages/feedsim/run.sh

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -688,10 +688,13 @@ main() {
688688
echo "rpc_dist.json: ENABLED (file=$rpc_dist_json)"
689689

690690
# Phase 5-B mock_services fanout. Point LeafNodeRank at the colocated
691-
# mock_services Thrift server orchestrated by run-feedsim-multi.sh on
692-
# port 21222. Without --rpc_dist_path, LeafNodeRank falls back to
693-
# legacy folly::futures::sleep.
694-
local mock_services_opts="--rpc_dist_path=$rpc_dist_json --mock_services_host=localhost --mock_services_port=21222"
691+
# mock_services Thrift server orchestrated by run-feedsim-multi.sh.
692+
# MOCK_SERVICES_PORT is set per-instance (21222 + inst_id) so each
693+
# feedsim instance talks to its OWN mock_services, eliminating
694+
# cross-instance queue contention. Defaults to 21222 for back-compat
695+
# with single-instance manual runs.
696+
local mock_port="${MOCK_SERVICES_PORT:-21222}"
697+
local mock_services_opts="--rpc_dist_path=$rpc_dist_json --mock_services_host=localhost --mock_services_port=${mock_port}"
695698
# Optional fanout-scale override (defaults to LeafNodeRank's
696699
# --rpc_fanout_scale=0.025 when the env var is unset). Lets sweep
697700
# scripts A/B test heavier outbound load without rebuilding.

0 commit comments

Comments
 (0)