Skip to content

Commit 65e0377

Browse files
excelle08facebook-github-bot
authored andcommitted
Make per-request RNGs deterministic by default (reproducible QPS)
Differential Revision: D115671541
1 parent 159b42a commit 65e0377

4 files changed

Lines changed: 125 additions & 53 deletions

File tree

packages/feedsim/third_party/src/workloads/ranking/DriverNodeRank.cc

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414

1515
#include <algorithm>
16+
#include <atomic>
17+
#include <cstdlib>
1618
#include <cstring>
1719
#include <iostream>
1820
#include <memory>
@@ -67,11 +69,40 @@ static std::shared_ptr<folly::CPUThreadPoolExecutor> g_session_pool;
6769
const int kMaxRequestSize = 8192;
6870
const int kRecomputeQPSPeriod = 1; // Reduced from 5 to 1 second for faster feedback
6971

72+
// Deterministic per-thread RNG seed. The driver's per-thread RNGs used to be
73+
// seeded from std::random_device, making the generated request stream (sizes,
74+
// content) non-reproducible run-to-run. Seed from a fixed base so each thread
75+
// keeps an independent but reproducible sequence. Set FEEDSIM_RNG_RANDOM=1 to
76+
// restore the old non-deterministic seed.
77+
static bool feedsimRngRandom() {
78+
static const bool kRandom = [] {
79+
const char* e = std::getenv("FEEDSIM_RNG_RANDOM");
80+
return e != nullptr && e[0] == '1';
81+
}();
82+
return kRandom;
83+
}
84+
85+
static unsigned detRngSeed(unsigned base) {
86+
if (feedsimRngRandom()) {
87+
return std::random_device{}();
88+
}
89+
static std::atomic<unsigned> ctr{0};
90+
return base + ctr.fetch_add(1) * 2654435761u;
91+
}
92+
93+
static unsigned detRngSeedTid(unsigned base, int thread_id) {
94+
if (feedsimRngRandom()) {
95+
return static_cast<unsigned>(std::random_device{}()) +
96+
static_cast<unsigned>(thread_id);
97+
}
98+
return base + static_cast<unsigned>(thread_id);
99+
}
100+
70101
// Simple random string generator (replaces oldisim/Util.h RandomString)
71102
static std::string RandomString(size_t length) {
72103
static const char charset[] =
73104
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
74-
thread_local std::mt19937 rng(std::random_device{}());
105+
thread_local std::mt19937 rng(detRngSeed(0xD5117A2Du));
75106
std::uniform_int_distribution<size_t> dist(0, sizeof(charset) - 2);
76107
std::string str(length, 0);
77108
for (size_t i = 0; i < length; ++i) {
@@ -202,15 +233,13 @@ void ThreadStartup(int thread_id,
202233

203234
// Initialize Silesia RNG per thread
204235
if (g_silesia_loader && g_silesia_loader->isLoaded()) {
205-
this_thread.silesia_rng.seed(
206-
static_cast<unsigned>(std::random_device{}()) + thread_id);
236+
this_thread.silesia_rng.seed(detRngSeedTid(0x51E51A00u, thread_id));
207237
}
208238

209239
// Initialize request size sampler RNG per thread
210240
if ((g_req_size_sampler && g_req_size_sampler->isLoaded()) ||
211241
g_rpc_dist_registry) {
212-
this_thread.req_size_rng.seed(
213-
static_cast<unsigned>(std::random_device{}()) + 0xDEADBEEF + thread_id);
242+
this_thread.req_size_rng.seed(detRngSeedTid(0xDEADBEEFu, thread_id));
214243
}
215244

216245
// If user gave QPS target, initialize QPS modulation

packages/feedsim/third_party/src/workloads/ranking/LeafNodeRank.cc

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <cmath>
1919
#include <cstdint>
2020
#include <cstdio>
21+
#include <cstdlib>
2122
#include <functional>
2223
#include <memory>
2324
#include <mutex>
@@ -311,10 +312,57 @@ static std::vector<ranking::RankingResponse> g_response_templates;
311312
static std::vector<size_t> g_response_template_sizes;
312313
static std::atomic<size_t> g_response_template_idx{0};
313314

315+
// Deterministic per-thread RNG seed. These per-request RNGs used to be seeded
316+
// from std::random_device, which made benchmark QPS non-reproducible run-to-run
317+
// (per-request response-size / fanout sampling shifted the SLA operating point;
318+
// see the T1_TRN ~4% QPS-CoV investigation). Seed from a fixed base mixed with
319+
// a per-thread counter so each thread keeps an independent but reproducible
320+
// sequence. Set FEEDSIM_RNG_RANDOM=1 to restore the old non-deterministic seed.
321+
static bool feedsimRngRandom() {
322+
static const bool kRandom = [] {
323+
const char* e = std::getenv("FEEDSIM_RNG_RANDOM");
324+
return e != nullptr && e[0] == '1';
325+
}();
326+
return kRandom;
327+
}
328+
329+
static unsigned detRngSeed(unsigned base) {
330+
if (feedsimRngRandom()) {
331+
return std::random_device{}();
332+
}
333+
static std::atomic<unsigned> ctr{0};
334+
return base + ctr.fetch_add(1) * 2654435761u;
335+
}
336+
337+
// Deterministic per-thread seed variant for call sites that already have a
338+
// stable thread index.
339+
static unsigned detRngSeedTid(unsigned base, int thread_id) {
340+
if (feedsimRngRandom()) {
341+
return std::random_device{}() ^ static_cast<unsigned>(thread_id + 1);
342+
}
343+
return base ^ static_cast<unsigned>(thread_id + 1);
344+
}
345+
346+
// Default deterministic seed for the logical workload RNGs (node/page/pointer).
347+
static constexpr unsigned kFeedsimDefaultSeed = 42u;
348+
349+
// Resolve a logical workload RNG seed. When the option is not passed we use a
350+
// fixed default (reproducible runs); an explicit negative value opts back into
351+
// a time-based seed for callers that want non-determinism.
352+
static unsigned resolveSeed(bool given, long arg, unsigned deflt) {
353+
if (!given) {
354+
return deflt;
355+
}
356+
if (arg < 0) {
357+
return static_cast<unsigned>(
358+
std::chrono::system_clock::now().time_since_epoch().count());
359+
}
360+
return static_cast<unsigned>(arg);
361+
}
362+
314363
// Pool of per-thread RNGs for response-size sampling (per-thread to avoid
315364
// shared-state contention; std::mt19937 is not thread-safe).
316-
static thread_local std::mt19937 g_response_size_rng{
317-
std::random_device{}()};
365+
static thread_local std::mt19937 g_response_size_rng{detRngSeed(0xF00D5127u)};
318366

319367
// Helper: measure the serialized size of a RankingResponse via CompactProto.
320368
// Used at pool init to build the (size → template) lookup table.
@@ -476,8 +524,7 @@ void ThreadStartup(
476524
// used elsewhere in this struct.
477525
this_thread.rpc_registry = g_rpc_registry.get();
478526
this_thread.rpc_silesia = g_rpc_silesia;
479-
this_thread.rpc_rng.seed(
480-
std::random_device{}() ^ static_cast<unsigned>(thread_id + 1));
527+
this_thread.rpc_rng.seed(detRngSeedTid(0x2C519A00u, thread_id));
481528
if (this_thread.rpc_registry != nullptr && srEventBasePool != nullptr) {
482529
auto evbs = srEventBasePool->getAllEventBases();
483530
this_thread.mock_clients.reserve(evbs.size());
@@ -507,29 +554,18 @@ void ThreadStartup(
507554
// Store shared DLRM ranker
508555
this_thread.dlrm_ranker = shared_dlrm_ranker;
509556

510-
unsigned noderank_seed;
511-
if (args.node_rank_seed_given) {
512-
noderank_seed = static_cast<unsigned>(args.node_rank_seed_arg);
513-
} else {
514-
noderank_seed = std::chrono::system_clock::now().time_since_epoch().count();
515-
}
557+
unsigned noderank_seed = resolveSeed(
558+
args.node_rank_seed_given, args.node_rank_seed_arg, kFeedsimDefaultSeed);
516559

517-
unsigned pointer_chase_seed;
518-
if (args.pointer_chase_seed_given) {
519-
pointer_chase_seed = static_cast<unsigned>(args.pointer_chase_seed_arg);
520-
} else {
521-
pointer_chase_seed =
522-
std::chrono::system_clock::now().time_since_epoch().count();
523-
}
560+
unsigned pointer_chase_seed = resolveSeed(
561+
args.pointer_chase_seed_given,
562+
args.pointer_chase_seed_arg,
563+
kFeedsimDefaultSeed);
524564

525565
// Only initialize PageRank if we're using it
526566
if (g_workload_type == WorkloadType::PAGERANK) {
527-
unsigned page_rank_seed;
528-
if (args.page_rank_seed_given) {
529-
page_rank_seed = static_cast<unsigned>(args.page_rank_seed_arg);
530-
} else {
531-
page_rank_seed = std::chrono::system_clock::now().time_since_epoch().count();
532-
}
567+
unsigned page_rank_seed = resolveSeed(
568+
args.page_rank_seed_given, args.page_rank_seed_arg, kFeedsimDefaultSeed);
533569
auto graph = params.makeGraphCopy(g_shared_graph);
534570
this_thread.page_ranker = std::make_unique<ranking::dwarfs::PageRank>(
535571
std::move(graph), args.cpu_threads_arg, page_rank_seed);
@@ -637,8 +673,7 @@ void ThreadStartup(
637673
// used elsewhere in this struct.
638674
this_thread.rpc_registry = g_rpc_registry.get();
639675
this_thread.rpc_silesia = g_rpc_silesia;
640-
this_thread.rpc_rng.seed(
641-
std::random_device{}() ^ static_cast<unsigned>(thread_id + 1));
676+
this_thread.rpc_rng.seed(detRngSeedTid(0x2C519A00u, thread_id));
642677
if (this_thread.rpc_registry != nullptr && srEventBasePool != nullptr) {
643678
auto evbs = srEventBasePool->getAllEventBases();
644679
this_thread.mock_clients.reserve(evbs.size());
@@ -664,27 +699,16 @@ void ThreadStartup(
664699
}
665700
}
666701
}
667-
unsigned noderank_seed;
668-
if (args.node_rank_seed_given) {
669-
noderank_seed = static_cast<unsigned>(args.node_rank_seed_arg);
670-
} else {
671-
noderank_seed = std::chrono::system_clock::now().time_since_epoch().count();
672-
}
702+
unsigned noderank_seed = resolveSeed(
703+
args.node_rank_seed_given, args.node_rank_seed_arg, kFeedsimDefaultSeed);
673704

674-
unsigned page_rank_seed;
675-
if (args.page_rank_seed_given) {
676-
page_rank_seed = static_cast<unsigned>(args.page_rank_seed_arg);
677-
} else {
678-
page_rank_seed = std::chrono::system_clock::now().time_since_epoch().count();
679-
}
705+
unsigned page_rank_seed = resolveSeed(
706+
args.page_rank_seed_given, args.page_rank_seed_arg, kFeedsimDefaultSeed);
680707

681-
unsigned pointer_chase_seed;
682-
if (args.pointer_chase_seed_given) {
683-
pointer_chase_seed = static_cast<unsigned>(args.pointer_chase_seed_arg);
684-
} else {
685-
pointer_chase_seed =
686-
std::chrono::system_clock::now().time_since_epoch().count();
687-
}
708+
unsigned pointer_chase_seed = resolveSeed(
709+
args.pointer_chase_seed_given,
710+
args.pointer_chase_seed_arg,
711+
kFeedsimDefaultSeed);
688712

689713
this_thread.page_ranker = std::make_unique<ranking::dwarfs::PageRank>(
690714
std::move(graph), args.cpu_threads_arg, page_rank_seed);

packages/feedsim/third_party/src/workloads/ranking/LeafNodeRankCmdline.ggo

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ option "max_response_size" - "Maximum response size in bytes returned by the lea
3535
option "compression_data_size" - "Number of bytes to compress per request." int default="131072"
3636
option "rank_trials_per_thread" - "Number of iterations each CPU thread executes of rank work." int default="1"
3737
option "min_icache_iterations" - "At least this number of icache busting iteration will be executed." int default="0"
38-
option "node_rank_seed" - "Seed for random number generator. If not provided, current time will be used." long optional
39-
option "page_rank_seed" - "Seed for PageRank random number generator. If not provided, current time will be used." long optional
40-
option "pointer_chase_seed" - "Seed for PointerChase random number generator. If not provided, current time will be used." long optional
38+
option "node_rank_seed" - "Seed for random number generator. Default 42 for deterministic runs; pass -1 for a time-based (non-deterministic) seed." long optional
39+
option "page_rank_seed" - "Seed for PageRank random number generator. Default 42 for deterministic runs; pass -1 for a time-based (non-deterministic) seed." long optional
40+
option "pointer_chase_seed" - "Seed for PointerChase random number generator. Default 42 for deterministic runs; pass -1 for a time-based (non-deterministic) seed." long optional
4141
option "chase_iterations" - "Number of chases to execute on handler thread." int default="5120"
4242
option "io_chase_iterations" - "Number of chases to execute on IO threads." int default="5120"
4343
option "io_time_ms" - "Milliseconds to sleep emualting I/O offcpu." int default="200"

packages/feedsim/third_party/src/workloads/ranking/mock_services/MockServiceHandler.cc

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
#include "MockServiceHandler.h"
1616

1717
#include <algorithm>
18+
#include <atomic>
1819
#include <chrono>
20+
#include <cstdlib>
1921
#include <cstring>
2022
#include <random>
2123
#include <thread>
@@ -84,7 +86,24 @@ constexpr int32_t kSpinThresholdUs = 200;
8486
// or buggy client from triggering bad_alloc on small-RAM hosts.
8587
constexpr uint32_t kMaxResponseSize = 16 * 1024 * 1024;
8688

87-
thread_local std::mt19937 tlRng{std::random_device{}()};
89+
// Deterministic per-thread RNG seed. This thread_local RNG (mock response-body
90+
// generation) used to be seeded from std::random_device, making benchmark
91+
// output non-reproducible run-to-run. Seed from a fixed base mixed with a
92+
// per-thread counter so each thread keeps an independent but reproducible
93+
// sequence. Set FEEDSIM_RNG_RANDOM=1 to restore the old non-deterministic seed.
94+
static unsigned detRngSeed(unsigned base) {
95+
static const bool kRandom = [] {
96+
const char* e = std::getenv("FEEDSIM_RNG_RANDOM");
97+
return e != nullptr && e[0] == '1';
98+
}();
99+
if (kRandom) {
100+
return std::random_device{}();
101+
}
102+
static std::atomic<unsigned> ctr{0};
103+
return base + ctr.fetch_add(1) * 2654435761u;
104+
}
105+
106+
thread_local std::mt19937 tlRng{detRngSeed(0x5EED3110u)};
88107

89108
// Free function so async continuations can use it without capturing `this`.
90109
// Takes a shared_ptr by value so the SilesiaLoader outlives the continuation

0 commit comments

Comments
 (0)