Skip to content

Commit db7a7aa

Browse files
committed
Fail loudly when transfer staging is exhausted
A producer that could not seat a pull-serve marked the send done without transferring, so the consumer learned of the failure only by waiting out its transfer timeout; a consumer that could not seat a read failed it silently. The producer now waits for staging to free, bounded by the transfer deadline, and reports a transfer failure when a request still cannot be seated. A request larger than the staging can ever seat -- a fixed slot, or the whole per-transfer pool -- fails at once. The misses while waiting are not logged; the one failure message on each side carries the request, the pages asked, and the pages free, so an exhausted pool is attributed where it happens. Measured on the setup of the previous commit: at 4 fixed slots the load exhausted staging 1112 times across 139 requests, each visible only as a consumer-side failure. With staging on demand none occurred. Memory and performance results for on-demand staging itself are in the commit that introduces it, "Stage disaggregation transfers on demand instead of in fixed slots" (github.com//pull/722). During shutdown a producer waiting for staging stops instead of sleeping out its deadline, and the manager's destructor waits for every pull-serve worker before tearing down the state they read.
1 parent 3e57cf6 commit db7a7aa

2 files changed

Lines changed: 89 additions & 16 deletions

File tree

tpu_sync/core/kv_cache_manager_with_transfer.cc

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,14 @@ KVCacheManagerWithTransfer::KVCacheManagerWithTransfer(
620620

621621
KVCacheManagerWithTransfer::~KVCacheManagerWithTransfer() {
622622
StopControlServer();
623+
// Pull-serve workers read this object's state; nothing may be torn down
624+
// while one is still running.
625+
shutting_down_.store(true, std::memory_order_relaxed);
626+
{
627+
absl::MutexLock lock(pull_workers_mu_);
628+
pull_workers_mu_.Await(absl::Condition(
629+
+[](int* active) { return *active == 0; }, &active_pull_workers_));
630+
}
623631
push_pool_.reset();
624632
pull_pool_.reset();
625633
if (host_block_manager_ && !all_slots_.empty()) {
@@ -1556,6 +1564,13 @@ void KVCacheManagerWithTransfer::StartRead(
15561564
if (!staged.has_value()) {
15571565
// Request larger than the staging pool can seat: surface as a recv
15581566
// failure (the connector can recompute) rather than throwing.
1567+
LOG(ERROR) << "StartRead: cannot stage " << unique_local_bids.size()
1568+
<< " blocks for req_id=" << req_id
1569+
<< " (dynamic=" << dynamic_host_staging_
1570+
<< ", free_host_blocks="
1571+
<< host_block_manager_->num_free_blocks()
1572+
<< ", free_slots=" << free_slots_.size()
1573+
<< ", max_blocks=" << max_blocks_ << ")";
15591574
failed_recving_.insert(req_id);
15601575
return;
15611576
}
@@ -1923,9 +1938,13 @@ KVCacheManagerWithTransfer::AcquireRecvStagingLocked(int64_t num_blocks,
19231938
return blocks;
19241939
}
19251940
// Lock the pages so the pool's LRU cannot evict staging that is mid-flight.
1941+
// A miss is reported to the caller only: the producer retries it until its
1942+
// deadline and the consumer fails it at once, and each logs its own verdict.
19261943
auto allocated = host_block_manager_->Allocate(static_cast<int>(num_blocks),
19271944
/*lock=*/true);
1928-
if (!allocated.ok()) return std::nullopt;
1945+
if (!allocated.ok()) {
1946+
return std::nullopt;
1947+
}
19291948
entry->staged_host_blocks = *allocated;
19301949
std::vector<int64_t> blocks;
19311950
blocks.reserve(allocated->size());
@@ -2319,10 +2338,16 @@ void KVCacheManagerWithTransfer::ProcessPullStream(
23192338
}
23202339
}
23212340

2341+
{
2342+
absl::MutexLock lock(pull_workers_mu_);
2343+
++active_pull_workers_;
2344+
}
23222345
std::thread([this, uuid = req.uuid, remote_data_endpoints, src_block_ids,
23232346
dst_block_ids]() {
23242347
StartPushInternal(uuid, remote_data_endpoints, src_block_ids,
23252348
dst_block_ids);
2349+
absl::MutexLock lock(pull_workers_mu_);
2350+
--active_pull_workers_;
23262351
}).detach();
23272352
}
23282353

@@ -2335,24 +2360,66 @@ void KVCacheManagerWithTransfer::StartPushInternal(
23352360
// pool. Writing D2H straight to host[src_block_id] overflows the host buffer
23362361
// once a device block id exceeds num_host_blocks.
23372362
std::vector<int64_t> host_block_ids;
2338-
{
2339-
absl::MutexLock lock(mu_);
2340-
auto it = send_entries_.find(uuid);
2341-
if (it == send_entries_.end()) {
2342-
return;
2363+
const auto stage_deadline = DeadlineFromNow();
2364+
while (true) {
2365+
if (shutting_down_.load(std::memory_order_relaxed)) {
2366+
return; // the manager is being destroyed; its state goes with it
23432367
}
2344-
RecvEntry staging;
2345-
auto staged = AcquireRecvStagingLocked(
2346-
static_cast<int64_t>(src_block_ids.size()), &staging);
2347-
if (!staged.has_value()) {
2348-
done_sending_.insert(it->second->req_id);
2349-
ReleaseEntrySlotLocked(it->second);
2350-
send_entries_.erase(it);
2368+
{
2369+
absl::MutexLock lock(mu_);
2370+
auto it = send_entries_.find(uuid);
2371+
if (it == send_entries_.end()) {
2372+
return; // request cancelled while waiting for staging
2373+
}
2374+
// Staging that can never seat this request fails it now rather than
2375+
// after the deadline: a fixed slot holds max_blocks_ pages, the
2376+
// per-transfer pool holds total_blocks() pages.
2377+
const int64_t capacity = dynamic_host_staging_
2378+
? host_block_manager_->total_blocks()
2379+
: max_blocks_;
2380+
if (static_cast<int64_t>(src_block_ids.size()) > capacity) {
2381+
LOG(ERROR) << "StartPushInternal: request " << it->second->req_id
2382+
<< " needs " << src_block_ids.size() << " blocks but "
2383+
<< (dynamic_host_staging_ ? "the host staging pool holds "
2384+
: "a staging slot holds ")
2385+
<< capacity;
2386+
failed_recving_.insert(it->second->req_id);
2387+
ReleaseEntrySlotLocked(it->second);
2388+
send_entries_.erase(it);
2389+
return;
2390+
}
2391+
RecvEntry staging;
2392+
auto staged = AcquireRecvStagingLocked(
2393+
static_cast<int64_t>(src_block_ids.size()), &staging);
2394+
if (staged.has_value()) {
2395+
it->second->slot_idx = staging.slot_idx;
2396+
it->second->staged_host_blocks = std::move(staging.staged_host_blocks);
2397+
host_block_ids = std::move(*staged);
2398+
break;
2399+
}
2400+
}
2401+
// Staging exhausted: wait for in-flight sends to hand blocks back
2402+
// instead of reporting a send that never happened. The consumer's own
2403+
// deadline still bounds the total wait.
2404+
if (std::chrono::steady_clock::now() >= stage_deadline) {
2405+
absl::MutexLock lock(mu_);
2406+
auto it = send_entries_.find(uuid);
2407+
if (it != send_entries_.end()) {
2408+
LOG(ERROR) << "StartPushInternal: staging exhausted serving "
2409+
<< it->second->req_id << " (" << src_block_ids.size()
2410+
<< " blocks; free_host_blocks="
2411+
<< host_block_manager_->num_free_blocks()
2412+
<< ", total_host_blocks="
2413+
<< host_block_manager_->total_blocks()
2414+
<< ", free_slots=" << free_slots_.size()
2415+
<< "); reporting transfer failure";
2416+
failed_recving_.insert(it->second->req_id);
2417+
ReleaseEntrySlotLocked(it->second);
2418+
send_entries_.erase(it);
2419+
}
23512420
return;
23522421
}
2353-
it->second->slot_idx = staging.slot_idx;
2354-
it->second->staged_host_blocks = std::move(staging.staged_host_blocks);
2355-
host_block_ids = std::move(*staged);
2422+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
23562423
}
23572424

23582425
// Coalesce contiguous (device,host) block runs into a few large copies. With

tpu_sync/core/kv_cache_manager_with_transfer.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,12 @@ class KVCacheManagerWithTransfer : public kv_cache::KVCacheManagerBase {
470470
// fixed slots. A short request then costs its own pages rather than a
471471
// whole slot, so the same pool seats more transfers at once.
472472
bool dynamic_host_staging_ = false;
473+
474+
// Pull-serve workers launched by ProcessPullStream. The destructor waits
475+
// for them, and shutting_down_ ends a worker's staging wait early.
476+
std::atomic<bool> shutting_down_{false};
477+
absl::Mutex pull_workers_mu_;
478+
int active_pull_workers_ ABSL_GUARDED_BY(pull_workers_mu_) = 0;
473479
double timeout_s_ = 120.0;
474480
bool unsafe_skip_buffer_lock_ = true;
475481

0 commit comments

Comments
 (0)