Skip to content

Commit 0c5c0ae

Browse files
adityas-metafacebook-github-bot
authored andcommitted
Silent-failure ODS counters (#5850)
Summary: X-link: facebookresearch/FBGEMM#2768 Add OBC counters for 3 silent failure modes in the Raw Embedding Streaming (RES) path, closing the must-have bar of T273802725 (subtask of T269497764, RES code-side observability this half). Today these failure sites only emit `XLOG(ERR)` / `LOG(ERROR)` — nothing aggregates, so on-call cannot trigger off them or query them in ODS without `tail -f` on individual trainer hosts. Counters added (all via `TrainingPsOdsLogger::bumpKey`, OBC category `raw_embedding_streaming`): | Counter | Site | |---|---| | `res.fail.shard_size_mismatch` | `raw_embedding_streamer.cpp` — both the outer `indices.size(0) != weights.size(0)` guard and the per-shard `weights_masked.size(0) != rows_in_shard` guard in `tensor_stream()` | | `res.fail.set_embeddings_rpc` | `raw_embedding_streamer.cpp` — wraps `co_await res_client->co_setEmbeddings(req)` in try/catch; bumps then re-throws to preserve trainer-thread termination | | `res.fail.write_sparse_throw` | `TrainingPsHandler.cpp` `catch (const std::exception& e)` block at the `writeSparseListOfTensors` site in `co_stream_tensors` | Backend (addresses review: use OBC, not fb303): all three route through the existing `TrainingPsOdsLogger` OBC client. The handler already owned an `ods_logger_`; the fbgemm-side `RawEmbeddingStreamer` now holds a `TrainingPsOdsLogger` member (forward-declared in the OSS-mirrored header, constructed only when streaming is enabled). OBC reaches ODS through the host-level agent with no per-process fb303 export/scrape config, so this revision deletes the fb303 `ResFailCounters.h` helper (and its `call_once` / `addStatExportType` / `FOLLY_EXPORT` machinery) entirely. Counters bump AT the failure site (inside catch / right before `continue`) so they reflect what actually happened — applying the correctness lesson from D104076551 V4 (don't preemptively count attempts at the top of the function). Per-TBE-per-rank granularity is fine: OBC aggregates per (entity, key), so per-TBE bumps land on the same per-host key rather than separate rows. Existing log lines preserved (additive). Existing throw-on-failure semantics preserved — the `co_setEmbeddings` catch re-throws after bumping so trainer-thread termination is unchanged. Out of scope (intentional, per T269497764 boundary): - MPSC `weights_to_stream_queue_` near-full counter — queue is unbounded; deferred to the queue-depth gauge work in T273802756. - Detectors / alerts on these counters — downstream consumer work. Reviewed By: FriedCosey Differential Revision: D107811590
1 parent 80bd3c0 commit 0c5c0ae

3 files changed

Lines changed: 93 additions & 2 deletions

File tree

fbgemm_gpu/include/fbgemm_gpu/split_embeddings_cache/raw_embedding_streamer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
#include <utility>
1616

17+
#ifdef FBGEMM_FBCODE
18+
namespace facebook::aiplatform::gmpp::experimental::training_ps {
19+
class TrainingPsOdsLogger;
20+
} // namespace facebook::aiplatform::gmpp::experimental::training_ps
21+
#endif
22+
1723
namespace fbgemm_gpu {
1824

1925
struct StreamQueueItem {
@@ -125,6 +131,12 @@ class RawEmbeddingStreamer : public torch::jit::CustomClassHolder {
125131
std::unique_ptr<std::thread> weights_stream_thread_;
126132
folly::UMPSCQueue<StreamQueueItem, true> weights_to_stream_queue_;
127133
std::unique_ptr<std::thread> stream_tensor_copy_thread_;
134+
// OBC logger for RES silent-failure counters (res.fail.*). Emits to the
135+
// host-level OBC agent, so it reaches ODS from the trainer process without
136+
// per-process fb303 scrape config. Only constructed when streaming is on.
137+
std::unique_ptr<facebook::aiplatform::gmpp::experimental::training_ps::
138+
TrainingPsOdsLogger>
139+
ods_logger_;
128140
#endif
129141
};
130142

fbgemm_gpu/src/split_embeddings_cache/raw_embedding_streamer.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <folly/coro/BlockingWait.h>
1111
#include <folly/stop_watch.h>
1212
#include <utility>
13+
#include "aiplatform/gmpp/experimental/training_ps/TrainingPsOdsLogger.h"
1314
#include "aiplatform/gmpp/experimental/training_ps/gen-cpp2/TrainingParameterServerService.h"
1415
#include "caffe2/torch/fb/distributed/wireSerializer/WireSerializer.h"
1516
#include "servicerouter/client/cpp2/ClientParams.h"
@@ -49,6 +50,7 @@ get_res_client(int64_t res_server_port) {
4950
TrainingParameterServerService>>(
5051
"realtime.delta.publish.esr", params);
5152
}
53+
5254
#endif
5355

5456
/// Read a scalar value from a tensor that is maybe a UVM tensor
@@ -167,6 +169,9 @@ RawEmbeddingStreamer::RawEmbeddingStreamer(
167169
// The first call to get the client is expensive, so eagerly get it here
168170
auto _eager_client = get_res_client(res_server_port_);
169171

172+
ods_logger_ = std::make_unique<facebook::aiplatform::gmpp::experimental::
173+
training_ps::TrainingPsOdsLogger>();
174+
170175
weights_stream_thread_ = std::make_unique<std::thread>([this] {
171176
while (!stop_) {
172177
auto stream_item_ptr = weights_to_stream_queue_.try_peek();
@@ -319,6 +324,9 @@ folly::coro::Task<void> RawEmbeddingStreamer::tensor_stream(
319324
XLOG(ERR) << "[TBE_ID" << unique_id_
320325
<< "] Indices and weights size mismatched " << indices.size(0)
321326
<< " " << weights.size(0);
327+
if (ods_logger_) {
328+
ods_logger_->bumpKey("shard_size_mismatch", 1);
329+
}
322330
co_return;
323331
}
324332
folly::stop_watch<std::chrono::milliseconds> stop_watch;
@@ -399,6 +407,9 @@ folly::coro::Task<void> RawEmbeddingStreamer::tensor_stream(
399407
<< "] don't send the request for size mismatched tensors table: "
400408
<< rows_in_shard << " weights: " << weights_masked.size(0)
401409
<< " global_indices: " << global_indices_masked.numel();
410+
if (ods_logger_) {
411+
ods_logger_->bumpKey("shard_size_mismatch", 1);
412+
}
402413
continue;
403414
}
404415
SetEmbeddingsRequest req;
@@ -420,7 +431,17 @@ folly::coro::Task<void> RawEmbeddingStreamer::tensor_stream(
420431
req.runtimeMeta() =
421432
torch::distributed::wireDumpTensor(runtime_meta_masked);
422433
}
423-
co_await res_client->co_setEmbeddings(req);
434+
try {
435+
co_await res_client->co_setEmbeddings(req);
436+
} catch (const std::exception& e) {
437+
if (ods_logger_) {
438+
ods_logger_->bumpKey("set_embeddings_rpc", 1);
439+
}
440+
XLOG(ERR) << "[TBE_ID" << unique_id_
441+
<< "] co_setEmbeddings threw on shard " << i << ": "
442+
<< e.what();
443+
throw;
444+
}
424445
}
425446
co_return;
426447
}

fbgemm_gpu/src/split_embeddings_cache/tests/raw_embedding_streamer_test.cpp

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,61 @@ TEST(RawEmbeddingStreamerTest, TestStreamE2E) {
237237
streamer->join_weights_stream_thread();
238238
}
239239

240+
TEST(RawEmbeddingStreamerTest, TestCoSetEmbeddingsThrowPropagates) {
241+
std::vector<std::string> table_names = {"tb1", "tb2", "tb3"};
242+
std::vector<int64_t> table_offsets = {0, 100, 300};
243+
std::vector<int64_t> table_sizes = {0, 50, 200, 300};
244+
245+
auto streamer = getRawEmbeddingStreamer(
246+
"test_set_embeddings_throw",
247+
true,
248+
table_names,
249+
table_offsets,
250+
table_sizes);
251+
252+
auto mock_service = std::make_shared<MockTrainingParameterServerService>();
253+
auto mock_server =
254+
std::make_shared<apache::thrift::ScopedServerInterfaceThread>(
255+
mock_service,
256+
"::1",
257+
0,
258+
facebook::services::TLSConfig::applyDefaultsToThriftServer);
259+
auto& mock_client_factory =
260+
facebook::servicerouter::getMockSRClientFactory(false /* strict */);
261+
mock_client_factory.registerMockService(
262+
"realtime.delta.publish.esr", mock_server);
263+
264+
EXPECT_CALL(*mock_service, co_setEmbeddings(_))
265+
.WillRepeatedly(
266+
folly::coro::gmock_helpers::CoInvoke(
267+
[](std::unique_ptr<aiplatform::gmpp::experimental::training_ps::
268+
SetEmbeddingsRequest>)
269+
-> folly::coro::Task<
270+
std::unique_ptr<aiplatform::gmpp::experimental::
271+
training_ps::SetEmbeddingsResponse>> {
272+
throw std::runtime_error("simulated RPC failure");
273+
co_return std::make_unique<
274+
aiplatform::gmpp::experimental::training_ps::
275+
SetEmbeddingsResponse>();
276+
}));
277+
278+
auto indices = at::tensor(
279+
{10, 2, 1, 150, 170, 230, 280},
280+
at::TensorOptions().device(at::kCPU).dtype(at::kLong));
281+
auto weights = at::randn(
282+
{indices.size(0), EMBEDDING_DIMENSION},
283+
at::TensorOptions().device(at::kCPU).dtype(c10::kFloat));
284+
285+
// Thrift repackages server-side exceptions into TApplicationException, so
286+
// assert that the exception still propagates (preserved contract). The
287+
// catch block bumps set_embeddings_rpc via the OBC logger before
288+
// rethrowing; OBC counters are not in-process readable, so the bump itself
289+
// is not unit-asserted here (the logger has no in-test sink).
290+
EXPECT_ANY_THROW(
291+
folly::coro::blockingWait(streamer->tensor_stream(
292+
indices, weights, std::nullopt, std::nullopt)));
293+
}
294+
240295
TEST(RawEmbeddingStreamerTest, TestMismatchedIndicesWeights) {
241296
std::vector<std::string> table_names = {"tb1", "tb2", "tb3"};
242297
std::vector<int64_t> table_offsets = {0, 100, 300};
@@ -258,7 +313,10 @@ TEST(RawEmbeddingStreamerTest, TestMismatchedIndicesWeights) {
258313
mock_client_factory.registerMockService(
259314
"realtime.delta.publish.esr", mock_server);
260315

261-
// Test with mismatched sizes - should not call service
316+
// Test with mismatched sizes - should not call service. The
317+
// shard_size_mismatch counter is bumped in this path, but OBC
318+
// counters are not in-process readable, so we verify the behavior (no RPC)
319+
// rather than the counter value.
262320
auto indices = at::tensor(
263321
{10, 2, 1}, at::TensorOptions().device(at::kCPU).dtype(at::kLong));
264322
auto weights = at::randn(

0 commit comments

Comments
 (0)