Skip to content

Commit 6c0640a

Browse files
committed
Stage resharding transfers in plan-allocated host blocks
Resharding resolved every block on the wire by identity into the host mirror: a sender staged each device block into the host block of the same id, and a receiver landed each incoming block at its destination id and uploaded from there. That only works when the host mirror spans the whole device block space, so both sides of a reshard pinned a host copy of their entire device KV pool. Under TPU_RAIDEN_DYNAMIC_HOST_STAGING=1 a plan now allocates host blocks for the device blocks it names -- its source blocks on a sender, its destination blocks on a receiver -- and records the mapping. Chunk resolution consults the mapping, the receiver's upload reads from the mapped blocks, and the blocks return to the pool when the receive completes or the plan is unregistered. The wire is unchanged: it still names device blocks. Senders obtain their staging blocks through plan_host_blocks() and copy into those instead of into the mirror. A demand-staged receiver plan is dropped when its transfer settles, whether or not it was unregistered first, so its mapping never outlives the blocks it names; until then, pushes already accepted keep landing in the plan's blocks, and unregistering a plan that already settled is a no-op. plan_host_blocks() refuses a plan that is not registered, a pool-addressed plan, and a block the plan's schedules do not name. Registering and unregistering a plan serialize with each other, and a plan becomes visible only after the staging and receive state that belong to it exist, so neither can observe the other half-done. Each registration carries a generation, and a receive's settlement cleanup only removes the registration it belongs to, never a newer one reusing the same uuid. Off by default. Pool-addressed plans keep their existing addressing. **Validation** 8->4 reshard (prefill TP8 -> decode attention TP4 x DP2), Qwen3-1.7B on v7x, 128-token pages, 8k context, 64 x 512-token requests at 64-way concurrency. Flag off vs on, same build and load, two runs per arm. The build includes the multi-sender completion fix for pushed transfers. | | off | on (512-block pool) | |---|---|---| | host staging per rank | whole device pool mirrored: 1025 blocks, 1.8 GB prefill / 3.5 GB decode | 512 blocks, 0.9 GB / 1.8 GB | | pinned host memory, both hosts | 42 GB | 21 GB | | gsm8k, 200 questions (symmetric serve: 0.675, 0.705) | 0.690, 0.670 | 0.685, 0.665 | | greedy probe, 5 sequential + 8 concurrent repeats | repeatable, equal to the symmetric serve | same | | req/s | 12.1, 12.8 | 13.0, 12.4 | | mean TTFT / ITL, ms | 1187 / 28.2, 1225 / 27.0 | 1571 / 23.3, 1208 / 26.8 | | failed transfers | 0 | 0 | - The pool no longer tracks the device pool, so the saving grows with the model: on gpt-oss-120b TP8 the mirror is 56.5 GiB per rank (452 GiB per host); the same 64 in-flight requests fit in about 2.3 GiB per host. - One probe prompt alternates between two sane continuations across its repeats, in both arms.
1 parent 89ef10d commit 6c0640a

9 files changed

Lines changed: 617 additions & 63 deletions

tpu_sync/api/torch/kv_cache_manager.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,25 @@ def unregister_active_plan(self, uuid: int) -> None:
207207
"""Removes a previously registered strided push plan."""
208208
self._impl.unregister_active_plan(uuid)
209209

210+
def plan_host_blocks(self, uuid: int, block_ids: Sequence[int]) -> List[int]:
211+
"""Host blocks staging ``block_ids`` under a registered plan.
212+
213+
A sender stages its device blocks into these before pushing.
214+
215+
Args:
216+
uuid: The plan's identifier, as passed to ``register_active_plan``.
217+
block_ids: Device block ids the plan names.
218+
219+
Returns:
220+
One host block id per entry of ``block_ids``; the ids themselves when
221+
the plan stages blocks at their own ids.
222+
223+
Raises:
224+
RuntimeError: The plan is not registered, or a block is not staged by
225+
the plan.
226+
"""
227+
return [int(b) for b in self._impl.plan_host_blocks(uuid, list(block_ids))]
228+
210229
def push_registered_plan(
211230
self,
212231
uuid: int,

tpu_sync/core/kv_cache_manager_with_transfer.cc

Lines changed: 204 additions & 59 deletions
Large diffs are not rendered by default.

tpu_sync/core/kv_cache_manager_with_transfer.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ class KVCacheManagerWithTransfer : public kv_cache::KVCacheManagerBase {
204204
absl::Status RegisterActivePlan(
205205
uint64_t uuid, const ::tpu_sync::rpc::StartTransferRequest& request,
206206
bool is_sender) override;
207+
absl::Status UnregisterActivePlan(uint64_t uuid) override;
208+
// Drops the plan of a receive that has settled; a plan already gone, or
209+
// a newer registration reusing the uuid, is left alone.
210+
void UnregisterSettledPlan(uint64_t uuid, uint64_t generation);
207211

208212
absl::Status RegisterRecv(uint64_t uuid, const std::string& req_id,
209213
int64_t expected_block_count) override;
@@ -341,6 +345,18 @@ class KVCacheManagerWithTransfer : public kv_cache::KVCacheManagerBase {
341345
Slot AcquireSlotLocked();
342346
void ReleaseSlotLocked(int64_t slot_idx);
343347
struct RecvEntry; // defined below; staging helpers take it by pointer
348+
// Serializes plan registration and unregistration, so a plan is never
349+
// published without its staging owner or torn down against a half-built
350+
// registration.
351+
absl::Mutex plan_lifecycle_mu_;
352+
// Source of plan generations: the uuid names a transfer, a generation
353+
// names one registration of it (the same uuid may come back, e.g. on a
354+
// retry of the same transfer).
355+
uint64_t plan_generation_counter_ ABSL_GUARDED_BY(plan_lifecycle_mu_) = 0;
356+
// Host staging held by a plan: a sender's, or a receiver's whose
357+
// destination is host memory. Released when the plan is unregistered.
358+
absl::flat_hash_map<uint64_t, std::vector<int>> plan_staging_
359+
ABSL_GUARDED_BY(mu_);
344360
// Host staging for one incoming read: exactly `num_blocks` blocks under
345361
// demand staging, a whole fixed slot otherwise. Returns nullopt when the
346362
// staging pool cannot seat the request.
@@ -399,6 +415,15 @@ class KVCacheManagerWithTransfer : public kv_cache::KVCacheManagerBase {
399415
std::chrono::steady_clock::time_point start_time;
400416
std::vector<raiden::PjRtCopyFuture> h2d_futures;
401417
bool is_pool_reshard = false;
418+
// The plan is dropped when this receive settles: set for every
419+
// demand-staged receiver plan (whose mapping would otherwise outlive its
420+
// freed blocks) and when an unregister arrives while the receive is in
421+
// flight (the plan stays mapped until then so late pushes resolve
422+
// through its blocks).
423+
bool unregister_on_settle = false;
424+
// Generation of the plan this receive belongs to; settlement cleanup
425+
// only touches that registration.
426+
uint64_t plan_generation = 0;
402427
std::set<size_t> expected_pool_indices;
403428
std::set<size_t> started_pool_indices;
404429
std::set<size_t> completed_pool_indices;

tpu_sync/core/kv_cache_manager_with_transfer_pool_reshard_test.cc

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
#include <limits>
2525
#include <memory>
2626
#include <optional>
27+
#include <chrono>
2728
#include <string>
29+
#include <thread>
30+
#include <tuple>
2831
#include <utility>
2932
#include <vector>
3033

@@ -43,7 +46,9 @@ namespace {
4346

4447
using ::testing::_;
4548
using ::testing::Ge;
49+
using ::testing::Contains;
4650
using ::testing::IsEmpty;
51+
using ::tpu_sync::rpc::MEMORY_TYPE_DRAM;
4752
using ::tpu_sync::rpc::MEMORY_TYPE_HBM;
4853
using ::tpu_sync::rpc::ShardPushEntryProto;
4954
using ::tpu_sync::rpc::StartTransferRequest;
@@ -68,6 +73,9 @@ class TestManager : public KVCacheManagerWithTransfer {
6873
// that never touch the holds (validation and the no-bytes-owned sender
6974
// completion) may rely on it.
7075
void AttachPlaceholderDeviceHold() { buffer_holds_.emplace_back(); }
76+
// Stages plans in per-transfer host blocks, as
77+
// TPU_RAIDEN_DYNAMIC_HOST_STAGING=1 does for device-attached managers.
78+
void EnableDemandStaging() { dynamic_host_staging_ = true; }
7179
};
7280

7381
kv_cache::PoolSpec DensePool(std::string tag, int64_t block_stride = 128,
@@ -126,6 +134,34 @@ StartTransferRequest ValidPlan(
126134
return plan;
127135
}
128136

137+
// A block-addressed plan (no pool groups) that moves `src` to `dst` on one
138+
// shard, destined for HBM or for host memory.
139+
StartTransferRequest BlockPlan(int64_t uuid, const std::vector<int32_t>& src,
140+
const std::vector<int32_t>& dst,
141+
::tpu_sync::rpc::MemoryType dst_mem_type) {
142+
StartTransferRequest plan;
143+
plan.set_uuid(uuid);
144+
plan.set_req_id("block_plan_req_" + std::to_string(uuid));
145+
plan.set_dst_mem_type(dst_mem_type);
146+
plan.set_use_block_chunks(true);
147+
plan.set_parallelism(1);
148+
auto* schedule = &(*plan.mutable_shard_push_schedules())[0];
149+
for (size_t i = 0; i < src.size(); ++i) {
150+
auto* entry = schedule->add_entries();
151+
entry->set_dst_peer("127.0.0.1:1");
152+
entry->set_dst_shard_idx(0);
153+
entry->set_src_block_id(src[i]);
154+
entry->set_dst_block_id(dst[i]);
155+
entry->set_src_offset_bytes(0);
156+
entry->set_dst_offset_bytes(0);
157+
entry->set_size_bytes(16);
158+
entry->set_src_stride_bytes(0);
159+
entry->set_dst_stride_bytes(0);
160+
entry->set_count(1);
161+
}
162+
return plan;
163+
}
164+
129165
void ExpectInvalid(const absl::Status& status, const std::string& fragment) {
130166
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument)
131167
<< status.ToString();
@@ -686,5 +722,198 @@ TEST(PoolReshardRecvTest, FinishPoolReshardRecvDoesNotRecordMetricOnFailure) {
686722
absl::InternalError("simulated failure"));
687723
}
688724

725+
726+
TEST(SendDeadlineTest, ExpiredSendEntryFailsInsteadOfReportingDone) {
727+
TestManager manager(/*timeout_s=*/0.05);
728+
ASSERT_GT(manager.NotifyForRead("expired_send_req", 31, {0, 1}), 0);
729+
730+
std::this_thread::sleep_for(std::chrono::milliseconds(120));
731+
const auto [done_sending, done_recving, failed_recving] =
732+
manager.CompleteReadRaw();
733+
EXPECT_THAT(done_sending, IsEmpty());
734+
EXPECT_THAT(failed_recving, Contains("expired_send_req"));
735+
}
736+
737+
TEST(DemandStagingTest, SenderPlanReturnsStagingOnUnregister) {
738+
TestManager manager;
739+
manager.EnableDemandStaging();
740+
auto* pool = manager.host_block_manager();
741+
const int free_before = pool->num_free_blocks();
742+
ASSERT_TRUE(manager
743+
.RegisterActivePlan(21, BlockPlan(21, {0, 1}, {2, 3},
744+
MEMORY_TYPE_HBM),
745+
/*is_sender=*/true)
746+
.ok());
747+
EXPECT_EQ(pool->num_free_blocks(), free_before - 2);
748+
EXPECT_EQ(pool->num_locked_blocks(), 2);
749+
ASSERT_TRUE(manager.UnregisterActivePlan(21).ok());
750+
EXPECT_EQ(pool->num_free_blocks(), free_before);
751+
EXPECT_EQ(pool->num_locked_blocks(), 0);
752+
}
753+
754+
TEST(DemandStagingTest, HostReceiverPlanReturnsStagingOnUnregister) {
755+
TestManager manager;
756+
manager.EnableDemandStaging();
757+
auto* pool = manager.host_block_manager();
758+
const int free_before = pool->num_free_blocks();
759+
ASSERT_TRUE(manager
760+
.RegisterActivePlan(22, BlockPlan(22, {0, 1}, {2, 3},
761+
MEMORY_TYPE_DRAM),
762+
/*is_sender=*/false)
763+
.ok());
764+
EXPECT_EQ(pool->num_free_blocks(), free_before - 2);
765+
EXPECT_EQ(pool->num_locked_blocks(), 2);
766+
ASSERT_TRUE(manager.UnregisterActivePlan(22).ok());
767+
EXPECT_EQ(pool->num_free_blocks(), free_before);
768+
EXPECT_EQ(pool->num_locked_blocks(), 0);
769+
}
770+
771+
TEST(DemandStagingTest, DuplicateSenderRegistrationKeepsOriginalStaging) {
772+
TestManager manager;
773+
manager.EnableDemandStaging();
774+
auto* pool = manager.host_block_manager();
775+
const int free_before = pool->num_free_blocks();
776+
const StartTransferRequest plan =
777+
BlockPlan(23, {0, 1}, {2, 3}, MEMORY_TYPE_HBM);
778+
ASSERT_TRUE(manager.RegisterActivePlan(23, plan, /*is_sender=*/true).ok());
779+
const int free_registered = pool->num_free_blocks();
780+
EXPECT_EQ(free_registered, free_before - 2);
781+
782+
// The second registration is refused and must neither keep its own
783+
// staging nor disturb the first plan's.
784+
const absl::Status duplicate =
785+
manager.RegisterActivePlan(23, plan, /*is_sender=*/true);
786+
EXPECT_EQ(duplicate.code(), absl::StatusCode::kAlreadyExists)
787+
<< duplicate.ToString();
788+
EXPECT_EQ(pool->num_free_blocks(), free_registered);
789+
EXPECT_EQ(pool->num_locked_blocks(), 2);
790+
791+
ASSERT_TRUE(manager.UnregisterActivePlan(23).ok());
792+
EXPECT_EQ(pool->num_free_blocks(), free_before);
793+
EXPECT_EQ(pool->num_locked_blocks(), 0);
794+
}
795+
796+
797+
TEST(DemandStagingTest, UnregisteringInFlightReceiverDefersUntilItSettles) {
798+
TestManager manager(/*timeout_s=*/0.05);
799+
manager.EnableDemandStaging();
800+
manager.AttachPlaceholderDeviceHold();
801+
auto* pool = manager.host_block_manager();
802+
const int free_before = pool->num_free_blocks();
803+
ASSERT_TRUE(manager
804+
.RegisterActivePlan(24, BlockPlan(24, {0, 1}, {2, 3},
805+
MEMORY_TYPE_HBM),
806+
/*is_sender=*/false)
807+
.ok());
808+
EXPECT_EQ(pool->num_free_blocks(), free_before - 2);
809+
const auto staged = manager.PlanHostBlocks(24, {2, 3});
810+
ASSERT_TRUE(staged.ok()) << staged.status();
811+
812+
// Unregistering while the receive is in flight keeps the plan's mapping
813+
// and its staging, so pushes already accepted still land in the plan's
814+
// blocks ...
815+
ASSERT_TRUE(manager.UnregisterActivePlan(24).ok());
816+
const auto still_staged = manager.PlanHostBlocks(24, {2, 3});
817+
ASSERT_TRUE(still_staged.ok()) << still_staged.status();
818+
EXPECT_EQ(*still_staged, *staged);
819+
EXPECT_EQ(pool->num_free_blocks(), free_before - 2);
820+
821+
// ... until the receive settles; here it times out. Then the plan, its
822+
// staging and the receive entry go together.
823+
std::this_thread::sleep_for(std::chrono::milliseconds(120));
824+
const auto [done_sending, done_recving, failed_recving] =
825+
manager.CompleteReadRaw();
826+
EXPECT_THAT(failed_recving, Contains("block_plan_req_24"));
827+
EXPECT_EQ(manager.PlanHostBlocks(24, {2, 3}).status().code(),
828+
absl::StatusCode::kNotFound);
829+
EXPECT_EQ(pool->num_free_blocks(), free_before);
830+
EXPECT_EQ(pool->num_locked_blocks(), 0);
831+
}
832+
833+
TEST(DemandStagingTest, PlanHostBlocksFailsClosed) {
834+
TestManager manager;
835+
manager.EnableDemandStaging();
836+
auto* pool = manager.host_block_manager();
837+
EXPECT_EQ(manager.PlanHostBlocks(25, {0}).status().code(),
838+
absl::StatusCode::kNotFound);
839+
840+
// Occupy the identity blocks first, so the plan's host blocks provably
841+
// differ from its device blocks.
842+
const auto occupied = pool->Allocate(2, /*lock=*/true);
843+
ASSERT_TRUE(occupied.ok());
844+
ASSERT_TRUE(manager
845+
.RegisterActivePlan(25, BlockPlan(25, {0, 1}, {2, 3},
846+
MEMORY_TYPE_HBM),
847+
/*is_sender=*/true)
848+
.ok());
849+
const auto staged = manager.PlanHostBlocks(25, {0, 1});
850+
ASSERT_TRUE(staged.ok()) << staged.status();
851+
ASSERT_EQ(staged->size(), 2u);
852+
EXPECT_NE(*staged, (std::vector<int64_t>{0, 1}));
853+
EXPECT_TRUE(pool->IsLocked(static_cast<int>((*staged)[0])));
854+
EXPECT_TRUE(pool->IsLocked(static_cast<int>((*staged)[1])));
855+
// A block the plan does not stage is refused rather than passed through.
856+
EXPECT_EQ(manager.PlanHostBlocks(25, {0, 7}).status().code(),
857+
absl::StatusCode::kInvalidArgument);
858+
859+
ASSERT_TRUE(manager.UnregisterActivePlan(25).ok());
860+
EXPECT_EQ(manager.PlanHostBlocks(25, {0}).status().code(),
861+
absl::StatusCode::kNotFound);
862+
}
863+
864+
TEST(DemandStagingTest, PlanHostBlocksIsIdentityForFixedStaging) {
865+
TestManager manager;
866+
ASSERT_TRUE(manager
867+
.RegisterActivePlan(26, BlockPlan(26, {0, 1}, {2, 3},
868+
MEMORY_TYPE_HBM),
869+
/*is_sender=*/true)
870+
.ok());
871+
const auto staged = manager.PlanHostBlocks(26, {0, 1});
872+
ASSERT_TRUE(staged.ok()) << staged.status();
873+
EXPECT_EQ(*staged, (std::vector<int64_t>{0, 1}));
874+
// Identity covers only the blocks the plan names.
875+
EXPECT_EQ(manager.PlanHostBlocks(26, {7}).status().code(),
876+
absl::StatusCode::kInvalidArgument);
877+
ASSERT_TRUE(manager.UnregisterActivePlan(26).ok());
878+
}
879+
880+
TEST(DemandStagingTest, PlanHostBlocksRejectsBlocksOfAnEmptyPlan) {
881+
TestManager manager;
882+
manager.EnableDemandStaging();
883+
ASSERT_TRUE(manager
884+
.RegisterActivePlan(28, BlockPlan(28, {}, {},
885+
MEMORY_TYPE_HBM),
886+
/*is_sender=*/true)
887+
.ok());
888+
EXPECT_EQ(manager.PlanHostBlocks(28, {0}).status().code(),
889+
absl::StatusCode::kInvalidArgument);
890+
ASSERT_TRUE(manager.UnregisterActivePlan(28).ok());
891+
}
892+
893+
TEST(DemandStagingTest, DemandStagedReceiverPlanUnregistersWhenItSettles) {
894+
TestManager manager(/*timeout_s=*/0.05);
895+
manager.EnableDemandStaging();
896+
manager.AttachPlaceholderDeviceHold();
897+
auto* pool = manager.host_block_manager();
898+
const int free_before = pool->num_free_blocks();
899+
ASSERT_TRUE(manager
900+
.RegisterActivePlan(27, BlockPlan(27, {0, 1}, {2, 3},
901+
MEMORY_TYPE_HBM),
902+
/*is_sender=*/false)
903+
.ok());
904+
ASSERT_TRUE(manager.PlanHostBlocks(27, {2, 3}).ok());
905+
906+
// Nobody unregisters; the plan still goes when the receive settles (here
907+
// by timeout), leaving neither a stale mapping nor held blocks behind.
908+
std::this_thread::sleep_for(std::chrono::milliseconds(120));
909+
const auto [done_sending, done_recving, failed_recving] =
910+
manager.CompleteReadRaw();
911+
EXPECT_THAT(failed_recving, Contains("block_plan_req_27"));
912+
EXPECT_EQ(manager.PlanHostBlocks(27, {2, 3}).status().code(),
913+
absl::StatusCode::kNotFound);
914+
EXPECT_EQ(pool->num_free_blocks(), free_before);
915+
EXPECT_EQ(pool->num_locked_blocks(), 0);
916+
}
917+
689918
} // namespace
690919
} // namespace tpu_raiden

tpu_sync/frameworks/torch/kv_cache_manager.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ class KVCacheManager {
238238
return torch_manager_->UnregisterActivePlan(uuid);
239239
}
240240

241+
absl::StatusOr<std::vector<int64_t>> PlanHostBlocks(
242+
uint64_t uuid, const std::vector<int64_t>& block_ids) {
243+
return torch_manager_->PlanHostBlocks(uuid, block_ids);
244+
}
245+
241246
absl::Status RegisterRecv(uint64_t uuid, const std::string& req_id,
242247
int64_t expected_block_count) {
243248
return torch_manager_->RegisterRecv(uuid, req_id, expected_block_count);

tpu_sync/frameworks/torch/tpu_raiden_host_module.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,11 @@ NB_MODULE(_tpu_raiden_host, m) {
242242
.def(
243243
"unregister_active_plan",
244244
[](HostKVCacheManager& self, uint64_t uuid) {
245-
ThrowIfError(self.UnregisterActivePlan(uuid),
245+
absl::Status status = self.UnregisterActivePlan(uuid);
246+
// A plan that already settled is gone; unregistering it again is
247+
// a no-op rather than an error.
248+
if (absl::IsNotFound(status)) return;
249+
ThrowIfError(status,
246250
"KVCacheManager unregister_active_plan failed");
247251
},
248252
nb::arg("uuid"))

tpu_sync/frameworks/torch/tpu_raiden_torch_module.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,27 @@ NB_MODULE(_tpu_raiden_torch, m) {
272272
"unregister_active_plan",
273273
[](KVCacheManager& self, uint64_t uuid) {
274274
absl::Status status = self.UnregisterActivePlan(uuid);
275+
// A plan that already settled is gone; unregistering it again is
276+
// a no-op rather than an error.
277+
if (absl::IsNotFound(status)) return;
275278
if (!status.ok()) {
276279
throw std::runtime_error(
277280
"KVCacheManager unregister_active_plan failed: " +
278281
std::string(status.message()));
279282
}
280283
},
281284
nb::arg("uuid"))
285+
.def(
286+
"plan_host_blocks",
287+
[](KVCacheManager& self, uint64_t uuid,
288+
const std::vector<int64_t>& block_ids) {
289+
auto blocks = self.PlanHostBlocks(uuid, block_ids);
290+
if (!blocks.ok()) {
291+
throw std::runtime_error(std::string(blocks.status().message()));
292+
}
293+
return *std::move(blocks);
294+
},
295+
nb::arg("uuid"), nb::arg("block_ids"))
282296
.def(
283297
"push_registered_plan",
284298
[](KVCacheManager& self, uint64_t uuid, const std::string& peer,

0 commit comments

Comments
 (0)