Skip to content

Commit 6b146f4

Browse files
excelle08meta-codesync[bot]
authored andcommitted
Pre-build 64-slot response template pool to eliminate per-request RPC-DataGen cost (#742)
Summary: Pull Request resolved: #742 The t31v6 BGM hot-function breakdown showed RPC-DataGen at 5.9% of total CPU, dominated by `SilesiaResponseGenerator::generateRankingResponse` (~4.65% leaf self-time) plus the matching `~RankingObject` / `~RankingStory` destructor chain (~1.23%) — every request constructs a fresh 2000-story RankingResponse, serializes it, then immediately destroys it. Total chain is ~5.5% of CPU that has no production analog (this is test-only data fabrication). Switch to a precomputed template pool: build `kResponseTemplatePoolSize=64` RankingResponse instances at server startup (right after `g_silesia_response_gen` is wired), and each request hands a `RankingResponse&` to the serializer by rotating through the pool. Only the `queryID` field is mutated per-request (the one field a real aggregator varies). 64 slots are enough to keep any single template from getting cache-hot enough to skew the Serialization/Compression bucket measurements. Three callsites updated: - `LeafNodeRank.cc:1216-1224` — DLRM async request handler Stage-5 (the dominant hot path) - `LeafNodeRank.cc:1419-1425` — second async response handler (sister to the above) - `LeafNodeRank.cc:1511-1513` — sync legacy ranking handler Init point added at `LeafNodeRank.cc:~2346` after the Silesia loader is up, so the templates correctly use Silesia-derived payload bytes when `--silesia_dir` is set (and fall back to the xor128 RNG path otherwise). Wire-shape is identical to today: same story count, same object count per story, same payload-map sizes, same action counts. The 64-slot rotation prevents any single template from being cache-hot enough to distort the Serialization or Compression CPU buckets. Reviewed By: YifanYuan3 Differential Revision: D107327607
1 parent 3d6f79f commit 6b146f4

1 file changed

Lines changed: 52 additions & 10 deletions

File tree

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

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,37 @@ static ranking::RankingResponse generateResponse(int num_objects) {
283283
return ranking::generators::generateRandomRankingResponse(num_objects);
284284
}
285285

286+
// Pre-built response template pool. Constructed once during server init
287+
// after --num_objects is parsed. Each request grabs one by index, mutates
288+
// only queryID (the only field a real aggregator would vary per-request),
289+
// then serializes. Eliminates ~5% of total CPU previously spent in
290+
// SilesiaResponseGenerator::generateRankingResponse + ~RankingObject
291+
// destructor chain (was RPC-DataGen=6.3% in the t31v5 profile; this drops
292+
// it to <0.5%).
293+
//
294+
// The 64-slot rotation prevents a single template from getting cache-hot
295+
// enough to skew the Serialization/Compression bucket measurements. The
296+
// queryID mutation is racy across threads but acceptable: we only need
297+
// wire-shape stability, not value correctness for the benchmark.
298+
static constexpr size_t kResponseTemplatePoolSize = 64;
299+
static std::vector<ranking::RankingResponse> g_response_templates;
300+
static std::atomic<size_t> g_response_template_idx{0};
301+
302+
static void initResponseTemplatePool(int num_objects) {
303+
g_response_templates.reserve(kResponseTemplatePoolSize);
304+
for (size_t i = 0; i < kResponseTemplatePoolSize; ++i) {
305+
g_response_templates.push_back(generateResponse(num_objects));
306+
}
307+
}
308+
309+
static ranking::RankingResponse& pickResponseTemplate() {
310+
size_t idx = g_response_template_idx.fetch_add(
311+
1, std::memory_order_relaxed) % kResponseTemplatePoolSize;
312+
ranking::RankingResponse& resp = g_response_templates[idx];
313+
resp.queryID() = static_cast<int64_t>(idx);
314+
return resp;
315+
}
316+
286317
#ifdef FEEDSIM_USE_DLRM
287318
void ThreadStartup(
288319
int thread_id,
@@ -1202,13 +1233,14 @@ void AsyncPageRankRequestHandler(
12021233
return total;
12031234
});
12041235
})
1205-
.thenValue([context_ptr, num_objects](int /*final_result*/) {
1206-
// Stage 5: Generate and send response.
1207-
ranking::RankingResponse resp = generateResponse(num_objects);
1208-
1236+
.thenValue([context_ptr](int /*final_result*/) {
1237+
// Stage 5: serialize a pre-built response template (see
1238+
// initResponseTemplatePool above). Replaces per-request response
1239+
// construction + destruction (was ~5% of total CPU under the
1240+
// RPC-DataGen category in the t31v5 profile).
1241+
ranking::RankingResponse& resp = pickResponseTemplate();
12091242
auto payloadiobufq = serializePayload(resp);
12101243
auto buf = payloadiobufq.move();
1211-
12121244
context_ptr->sendResponse(buf->data(), buf->length());
12131245
})
12141246
.thenError(folly::tag_t<std::exception>{}, [context_ptr](const std::exception& e) {
@@ -1404,8 +1436,10 @@ void DLRMRequestHandler(
14041436
return total;
14051437
});
14061438
})
1407-
.thenValue([context_ptr, num_objects](int /*final_result*/) {
1408-
auto resp = generateResponse(num_objects);
1439+
.thenValue([context_ptr](int /*final_result*/) {
1440+
// Use the pre-built template pool to avoid the ~5% RPC-DataGen
1441+
// construction + destructor cost per request.
1442+
ranking::RankingResponse& resp = pickResponseTemplate();
14091443
folly::IOBufQueue bufq;
14101444
apache::thrift::CompactSerializer::serialize(resp, &bufq);
14111445
auto buf = bufq.move();
@@ -1495,9 +1529,8 @@ void PageRankRequestHandler(
14951529
auto chaseFs = folly::collect(chaseFutures).get();
14961530
int chaseResult = std::accumulate(chaseFs.begin(), chaseFs.end(), 0);
14971531

1498-
// Generate a response
1499-
auto r = generateResponse(args.num_objects_arg);
1500-
ranking::RankingResponse resp = r;
1532+
// Use the pre-built template pool to avoid the ~5% RPC-DataGen cost.
1533+
ranking::RankingResponse& resp = pickResponseTemplate();
15011534

15021535
// Serialize into FBThrift
15031536
auto payloadiobufq = serializePayload(resp);
@@ -2331,6 +2364,15 @@ int main(int argc, char** argv) {
23312364
<< std::endl;
23322365
}
23332366

2367+
// Pre-build the response template pool now that generateResponse is wired
2368+
// (either Silesia or xor128). Each handler grabs a template by index +
2369+
// mutates queryID, eliminating the per-request construct+destruct chain
2370+
// that was 6.3% of total CPU (RPC-DataGen) in the t31v5 profile.
2371+
initResponseTemplatePool(args.num_objects_arg);
2372+
std::cout << "Response template pool: " << kResponseTemplatePoolSize
2373+
<< " pre-built " << args.num_objects_arg
2374+
<< "-object RankingResponse instances" << std::endl;
2375+
23342376
// Phase 5: load rpc_dist.json and instantiate the RpcDistRegistry. When
23352377
// --rpc_dist_path is empty (default) OR --use_legacy_sleep is set, the
23362378
// legacy folly::futures::sleep I/O simulation is used everywhere --

0 commit comments

Comments
 (0)