Skip to content

Commit 67b6697

Browse files
excelle08meta-codesync[bot]
authored andcommitted
Fix concurrent SIGSEGV in DLRM forward() and feature extractors (#739)
Summary: Pull Request resolved: #739 Two thread-safety bugs were causing leaf SIGSEGVs whenever requested QPS exceeded server saturation. The "collapse to 0.5 QPS" observed at over-saturation in t27/t28 was actually the leaf crashing, then the next driver phase achieving only the trickle of completions that landed before the crash. t27 forensic analysis (2026-05-28) found crashes at BGM/Grace q=100+/inst (100% of iters), CPL q=50+/inst (17-45% of iters depending on QPS). Two distinct crash sites observed: (A) `torch::jit::InterpreterStateImpl::runTemplate` -> `dlrmInferenceServerSide`. The existing per-clone LIFO checkout (`get_avail_thread_id` / `put_avail_thread_id`) was supposed to prevent concurrent `forward()` calls on the same Module clone (added in D105659812 / t16). It's likely that `torch::jit::Module::clone()` does not fully isolate JIT interpreter state — under high concurrency the leaf still crashed despite the LIFO. Added a per-`ThreadState` `std::mutex forward_mutex` as a safety net. Uncontended when the LIFO is working; serializes calls per clone if it's not, preventing the segfault. (B) `dcperf::feature_extractors::generated::vc_NNNN_NNNN` -> `std::vector<IdScorePair>::_M_realloc_insert` -> `je_large_dalloc`. The generated extractor code calls `c->example->idScoreLists[i].emplace_back(...)` and `c->structData[i] += ...`. `c->example` pointed at a single shared `flat_example_` member of `FeatureExtractorSuite`, and `c->structData` pointed at a single shared `flat_struct_data_` heap buffer. `LeafNodeRank::DLRMRequestHandler` dispatches `runFeatureExtraction(*this_thread_ptr, ...)` onto the multi-threaded folly GlobalCPUThread singleton, so multiple in-flight requests sharing the same `ThreadData` can hit `runFlatExtractors` concurrently on different worker threads, racing on those shared mutable members. Fixed by making both per-call (`thread_local MockFeatureExample local_example` + `thread_local std::vector<float> local_struct`), with `local_struct` snapshotted from the read-only `flat_struct_data_` template each call. Also converted `flat_pos_` from `size_t` to `std::atomic<size_t>` and reserve `count` slots in a single `fetch_add` so concurrent calls don't race the cursor. Read-only state (`flat_tables_`, `flat_hash_tables_`, `flat_features_`, `flat_copies_`) stays shared — extractor code only does `.find()` on tables and never mutates the others. Reviewed By: YifanYuan3 Differential Revision: D106731815
1 parent d1c6354 commit 67b6697

3 files changed

Lines changed: 86 additions & 23 deletions

File tree

packages/feedsim/third_party/src/workloads/ranking/dwarfs/dlrm.cpp

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,23 @@ struct DLRM::Impl {
3737
// worker thread owns a deep-cloned Module via model_copy, so
3838
// concurrent forward() calls touch disjoint state. See t16
3939
// SIGSEGV crash (2026-05-18) for the original failure.
40+
//
41+
// forward_mutex (t29, 2026-05-28): the LIFO checkout in
42+
// get_avail_thread_id() / put_avail_thread_id() is supposed to
43+
// prevent two callers from holding the same thread_id at once, but
44+
// the leaf still SIGSEGVs inside torch::jit::InterpreterStateImpl::
45+
// runTemplate at over-saturation (t27 q=160+/inst on BGM/Grace,
46+
// q=50+ on CPL). Likely cause: torch::jit::Module::clone() does NOT
47+
// fully isolate all interpreter state. Per-clone mutex is a defensive
48+
// safety net — uncontended when the LIFO is doing its job, prevents
49+
// the segfault if it's not.
4050
struct ThreadState {
4151
std::mt19937 rng;
4252
std::normal_distribution<float> dense_dist{0.0f, 1.0f};
4353
std::vector<float> dense_buffer;
4454
std::vector<int64_t> sparse_buffer;
4555
std::unique_ptr<torch::jit::script::Module> model_copy;
56+
std::mutex forward_mutex;
4657
};
4758
std::vector<std::unique_ptr<ThreadState>> thread_states;
4859

@@ -248,14 +259,17 @@ int DLRM::infer(int num_inferences, int batch_size) {
248259
params_.embedding_table_sizes);
249260

250261
// Run inference on the per-thread Module clone (not the shared
251-
// pimpl_->model) to avoid concurrent forward() race.
262+
// pimpl_->model) to avoid concurrent forward() race. forward_mutex
263+
// serializes calls per clone as a safety net on top of the LIFO
264+
// checkout — see ThreadState comment.
252265
std::vector<torch::jit::IValue> inputs;
253266
inputs.push_back(dense_tensor);
254267
inputs.push_back(sparse_tensor);
255268

256269
torch::NoGradGuard no_grad;
257-
auto& thread_model = *pimpl_->thread_states[thread_id]->model_copy;
258-
auto output = thread_model.forward(inputs).toTensor();
270+
auto& ts = *pimpl_->thread_states[thread_id];
271+
std::lock_guard<std::mutex> forward_lock(ts.forward_mutex);
272+
auto output = ts.model_copy->forward(inputs).toTensor();
259273

260274
// Count predictions (simulating actual work with the output)
261275
total_predictions += output.numel();
@@ -299,14 +313,16 @@ int DLRM::inferWithFeatures(
299313
{static_cast<int64_t>(batch_size), params_.num_sparse_features},
300314
torch::kInt64);
301315

302-
// Run inference on this thread's Module clone.
316+
// Run inference on this thread's Module clone. forward_mutex
317+
// serializes calls per clone — see ThreadState comment.
303318
std::vector<torch::jit::IValue> inputs;
304319
inputs.push_back(dense_tensor);
305320
inputs.push_back(sparse_tensor);
306321

307322
torch::NoGradGuard no_grad;
308-
auto& thread_model = *pimpl_->thread_states[thread_id]->model_copy;
309-
auto output = thread_model.forward(inputs).toTensor();
323+
auto& ts = *pimpl_->thread_states[thread_id];
324+
std::lock_guard<std::mutex> forward_lock(ts.forward_mutex);
325+
auto output = ts.model_copy->forward(inputs).toTensor();
310326

311327
// Count predictions (simulating actual work with the output)
312328
total_predictions += output.numel();

packages/feedsim/third_party/src/workloads/ranking/feature_extractors/FeatureExtractorSuite.cpp

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,44 @@ void FeatureExtractorSuite::runFlatExtractors(
135135
using namespace dcperf::feature_extractors::generated;
136136
if (flat_copies_.empty()) return;
137137

138-
flat_example_.resize(100, 50, 0);
138+
// Per-call mutable state. CopyContext fields the generated extractors
139+
// mutate (example via emplace_back, structData via in-place
140+
// arithmetic) MUST NOT alias across concurrent invocations on the
141+
// same suite — t27 forensic analysis showed SIGSEGV in
142+
// vc_NNNN_NNNN -> std::vector<IdScorePair>::_M_realloc_insert ->
143+
// je_large_dalloc, caused by concurrent emplace_back on the shared
144+
// flat_example_.idScoreLists[i]. thread_local keeps allocator
145+
// pressure low: each worker thread reuses its own buffers across
146+
// calls; only initialized on first call.
147+
thread_local MockFeatureExample local_example;
148+
local_example.resize(100, 50, 0);
149+
150+
// structData is mutated by the generated code even without
151+
// story_content seeding (e.g. `c->structData[off] += 1e-15f` in
152+
// archetype templates), so it also needs to be per-call. We snapshot
153+
// from flat_struct_data_ (the read-only template populated in
154+
// initializeFlatDispatch) into a thread_local buffer once per call.
155+
thread_local std::vector<float> local_struct;
156+
if (local_struct.size() != static_cast<size_t>(flat_struct_size_)) {
157+
local_struct.assign(flat_struct_data_.get(),
158+
flat_struct_data_.get() + flat_struct_size_);
159+
} else {
160+
std::copy(flat_struct_data_.get(),
161+
flat_struct_data_.get() + flat_struct_size_,
162+
local_struct.begin());
163+
}
164+
if (story_content && story_content_length > 0) {
165+
int len = std::min(story_content_length, flat_struct_size_);
166+
for (int i = 0; i < len; ++i) {
167+
local_struct[i] = static_cast<float>(story_content[i]) / 255.0f;
168+
}
169+
}
170+
139171
CopyContext ctx;
140172
ctx.tables = flat_tables_;
141-
ctx.structData = flat_struct_data_.get();
142-
ctx.structSize = flat_struct_size_;
143-
ctx.example = &flat_example_;
173+
ctx.structData = local_struct.data();
174+
ctx.structSize = static_cast<int>(local_struct.size());
175+
ctx.example = &local_example;
144176
ctx.features = flat_features_.data();
145177
ctx.numFeatures = static_cast<int>(flat_features_.size());
146178
ctx.queryKeys = input_sparse.data();
@@ -150,16 +182,16 @@ void FeatureExtractorSuite::runFlatExtractors(
150182
ctx.storyContent = story_content;
151183
ctx.storyContentLength = story_content_length;
152184

153-
// If story content is available, seed structData from it
154-
if (story_content && story_content_length > 0) {
155-
int len = std::min(story_content_length, flat_struct_size_);
156-
for (int i = 0; i < len; ++i) {
157-
flat_struct_data_[i] = static_cast<float>(story_content[i]) / 255.0f;
158-
}
159-
}
160-
185+
// Atomic cursor advance so concurrent calls don't race on
186+
// flat_pos_. Reserves `count` slots in one fetch_add; each call
187+
// therefore runs N consecutive copy functions from the shuffled
188+
// vector, just starting at a different offset depending on
189+
// interleaving. Distribution remains uniform.
190+
const size_t total = flat_copies_.size();
191+
size_t start = flat_pos_.fetch_add(static_cast<size_t>(count),
192+
std::memory_order_relaxed) %
193+
total;
161194
for (int i = 0; i < count; ++i) {
162-
flat_copies_[flat_pos_](&ctx);
163-
flat_pos_ = (flat_pos_ + 1) % flat_copies_.size();
195+
flat_copies_[(start + static_cast<size_t>(i)) % total](&ctx);
164196
}
165197
}

packages/feedsim/third_party/src/workloads/ranking/feature_extractors/FeatureExtractorSuite.h

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#pragma once
77

8+
#include <atomic>
89
#include <cstdint>
910
#include <memory>
1011
#include <random>
@@ -66,13 +67,27 @@ class FeatureExtractorSuite {
6667
std::vector<int64_t> sparse_buf_a_;
6768
std::vector<int64_t> sparse_buf_b_;
6869

69-
// Flat dispatch state
70+
// Flat dispatch state. Concurrency contract: a single
71+
// FeatureExtractorSuite instance can have runFlatExtractors() called
72+
// from multiple threads at once (LeafNodeRank dispatches feature
73+
// extraction onto the multi-threaded GlobalCPUThread pool, and
74+
// multiple in-flight requests on the same ThreadData share its
75+
// suite). The fields below are read-only after initializeFlatDispatch
76+
// — the generated extractor code only does `.find()` on tables and
77+
// never mutates flat_features_ / flat_copies_ / flat_hash_tables_,
78+
// so sharing is safe.
79+
//
80+
// The previously-member `flat_example_` and shared writes to
81+
// `flat_struct_data_` were removed: extractor code calls
82+
// `c->example->idScoreLists[i].emplace_back(...)` and `c->structData[i]
83+
// += ...`, both racy when shared. runFlatExtractors now uses
84+
// thread_local buffers for those, with the per-call copy from
85+
// flat_struct_data_ as the read-only template.
7086
std::vector<dcperf::feature_extractors::generated::CopyFn> flat_copies_;
71-
size_t flat_pos_ = 0;
87+
std::atomic<size_t> flat_pos_{0};
7288
std::unique_ptr<float[]> flat_struct_data_;
7389
int flat_struct_size_ = 0;
7490
std::unordered_map<int64_t, float> flat_tables_[4];
7591
dcperf::mock_hash::MockHashTable flat_hash_tables_[4];
76-
MockFeatureExample flat_example_;
7792
std::vector<MockFeature> flat_features_;
7893
};

0 commit comments

Comments
 (0)