@@ -54,21 +54,101 @@ HierarchyBlockManagerPool::HierarchyBlockManagerPool(
5454void HierarchyBlockManagerPool::deallocate (Sequence* sequence) {
5555 DCHECK (sequence != nullptr );
5656 int32_t dp_rank = BlockManagerPool::get_dp_rank (sequence);
57+ // Publish device KV blocks into the device prefix cache first, so that
58+ // offload-eligible blocks reach ref_count == 2 (held only by the sequence
59+ // state and the prefix-cache node). deallocate_for_sequence below re-runs
60+ // this cache step; it is idempotent (hashes recomputed from token ids find
61+ // the nodes inserted here), so moved-out blocks do not corrupt the cache.
5762 BlockManagerPool::cache (sequence);
5863
59- // Release host blocks if any
64+ collect_offload_pairs (sequence, dp_rank);
65+
66+ // Release the host blocks still held by the sequence. Blocks moved into the
67+ // offload queue are now invalid in this vector and are skipped by deallocate;
68+ // their host ids stay reserved (held by the queue) until the D2H copy
69+ // completes and the offload callback caches + frees them.
6070 auto host_blocks = sequence->host_kv_state ().blocks (BlockType::KV );
6171 if (!host_blocks.empty ()) {
6272 host_block_managers_[dp_rank]->deallocate (host_blocks);
6373 }
6474
65- // Release device blocks via the composite (includes prefix cache flush)
75+ // Release device blocks via the composite (includes prefix cache flush).
76+ // Offloaded device blocks were moved out above, so the KV leaf skips them;
77+ // the offload callback releases them once the copy is done.
6678 auto * composite =
6779 static_cast <CompositeBlockManager*>(block_managers_[dp_rank].get ());
6880 composite->deallocate_for_sequence (sequence);
6981 sequence->reset ();
7082}
7183
84+ void HierarchyBlockManagerPool::collect_offload_pairs (Sequence* sequence,
85+ int32_t dp_rank) {
86+ if (!options_.enable_prefix_cache ()) {
87+ return ;
88+ }
89+
90+ std::vector<Block>* device_blocks =
91+ sequence->kv_state ().mutable_blocks (BlockType::KV );
92+ std::vector<Block>* host_blocks =
93+ sequence->host_kv_state ().mutable_blocks (BlockType::KV );
94+ if (device_blocks == nullptr || device_blocks->empty ()) {
95+ return ;
96+ }
97+
98+ const size_t block_size = options_.block_size ();
99+ const size_t cached_host_block_num =
100+ sequence->host_kv_state ().kv_cache_tokens_num () / block_size;
101+ const size_t cached_device_block_num =
102+ sequence->kv_state ().kv_cache_tokens_num () / block_size;
103+
104+ const size_t host_block_num =
105+ host_blocks == nullptr ? 0 : host_blocks->size ();
106+ // Host already holds at least as many blocks as the device computed: nothing
107+ // new to offload.
108+ if (host_block_num >= device_blocks->size ()) {
109+ return ;
110+ }
111+
112+ // Allocate the host blocks needed to receive the device blocks that have no
113+ // host counterpart yet.
114+ const size_t needed_block_num = cached_device_block_num > host_block_num
115+ ? cached_device_block_num - host_block_num
116+ : 0 ;
117+ if (needed_block_num != 0 ) {
118+ std::vector<Block> new_host_blocks =
119+ host_block_managers_[dp_rank]->allocate (needed_block_num);
120+ if (new_host_blocks.size () != needed_block_num) {
121+ // Host pool exhausted; skip offload this round rather than partially
122+ // copy.
123+ return ;
124+ }
125+ sequence->add_host_blocks (BlockType::KV , new_host_blocks);
126+ host_blocks = sequence->host_kv_state ().mutable_blocks (BlockType::KV );
127+ }
128+ if (host_blocks == nullptr ) {
129+ return ;
130+ }
131+
132+ // Only offload blocks that are fully computed on device. In-batch prefix
133+ // cache insertion may register blocks before they are computed, so bound the
134+ // offload range by cached_device_block_num to avoid copying uncomputed data.
135+ const size_t offload_end_block_num = std::min (
136+ {cached_device_block_num, host_blocks->size (), device_blocks->size ()});
137+ for (size_t i = cached_host_block_num; i < offload_end_block_num; i++) {
138+ // ref_count == 2 means the block is held only by this sequence and the
139+ // prefix-cache node, i.e. it is uniquely owned and safe to offload. Beam
140+ // forks (shared blocks) have ref_count > 2 and are skipped.
141+ if (device_blocks->at (i).ref_count () != 2 ) {
142+ continue ;
143+ }
144+ host_blocks->at (i).set_hash_value (
145+ device_blocks->at (i).get_immutable_hash_value ());
146+ auto block_pair = std::make_shared<OffloadBlockPair>(
147+ std::move (device_blocks->at (i)), std::move (host_blocks->at (i)));
148+ offload_block_pair_queues_[dp_rank].enqueue (std::move (block_pair));
149+ }
150+ }
151+
72152bool HierarchyBlockManagerPool::allocate (Sequence* sequence,
73153 size_t num_tokens,
74154 size_t max_copy_in_blocks_num) {
@@ -97,9 +177,20 @@ bool HierarchyBlockManagerPool::allocate(Sequence* sequence,
97177 }
98178 auto hbm_blocks = sequence->kv_state ().blocks (BlockType::KV );
99179 auto host_blocks = sequence->host_kv_state ().blocks (BlockType::KV );
100- for (size_t i = hbm_cache_token_num / options_.block_size ();
101- i <
102- max_copy_in_blocks_num + (hbm_cache_token_num / options_.block_size ());
180+ // H2D copies host block i -> device block i, so i must index both vectors.
181+ // The host prefix match (host_cache_token_num) is computed over the full
182+ // prompt and can exceed the device blocks allocated for this (possibly
183+ // chunked) num_tokens, so clamp the copy range to the blocks that actually
184+ // exist on both sides to avoid out-of-bounds reads.
185+ const size_t hbm_block_begin = hbm_cache_token_num / options_.block_size ();
186+ const size_t copy_block_limit =
187+ std::min (hbm_blocks.size (), host_blocks.size ());
188+ if (hbm_block_begin + max_copy_in_blocks_num > copy_block_limit) {
189+ max_copy_in_blocks_num = copy_block_limit > hbm_block_begin
190+ ? copy_block_limit - hbm_block_begin
191+ : 0 ;
192+ }
193+ for (size_t i = hbm_block_begin; i < max_copy_in_blocks_num + hbm_block_begin;
103194 i++) {
104195 load_block_transfer_infos_[dp_rank].emplace_back (
105196 BlockTransferInfo (host_blocks[i].id (),
@@ -111,9 +202,7 @@ bool HierarchyBlockManagerPool::allocate(Sequence* sequence,
111202 size_t target_hbm_cache_token_num =
112203 max_copy_in_blocks_num == 0
113204 ? hbm_cache_token_num
114- : (max_copy_in_blocks_num +
115- (hbm_cache_token_num / options_.block_size ())) *
116- options_.block_size ();
205+ : (max_copy_in_blocks_num + hbm_block_begin) * options_.block_size ();
117206
118207 sequence->kv_state ().incr_kv_cache_tokens_num (target_hbm_cache_token_num -
119208 hbm_cache_token_num);
@@ -139,16 +228,27 @@ bool HierarchyBlockManagerPool::allocate(Sequence* sequence,
139228 auto hbm_blocks = sequence->kv_state ().blocks (BlockType::KV );
140229 auto host_blocks = sequence->host_kv_state ().blocks (BlockType::KV );
141230
142- for (size_t i = hbm_cache_token_num / options_.block_size ();
143- i < host_cache_token_num / options_.block_size ();
144- i++) {
231+ // H2D copies host block i -> device block i. host_cache_token_num is the
232+ // host prefix match over the full prompt and can exceed the device blocks
233+ // allocated for this (chunked) num_tokens, so clamp to the blocks present
234+ // on both sides to avoid out-of-bounds reads on hbm_blocks.
235+ const size_t copy_block_end =
236+ std::min ({host_cache_token_num / options_.block_size (),
237+ hbm_blocks.size (),
238+ host_blocks.size ()});
239+ const size_t hbm_block_begin = hbm_cache_token_num / options_.block_size ();
240+ for (size_t i = hbm_block_begin; i < copy_block_end; i++) {
145241 load_block_transfer_infos_[dp_rank].emplace_back (
146242 BlockTransferInfo (host_blocks[i].id (),
147243 hbm_blocks[i].id (),
148244 host_blocks[i].get_immutable_hash_value (),
149245 TransferType::H2D ));
150246 }
151- sequence->kv_state ().incr_kv_cache_tokens_num (host_cache_token_num -
247+ const size_t target_hbm_cache_token_num =
248+ copy_block_end > hbm_block_begin
249+ ? copy_block_end * options_.block_size ()
250+ : hbm_cache_token_num;
251+ sequence->kv_state ().incr_kv_cache_tokens_num (target_hbm_cache_token_num -
152252 hbm_cache_token_num);
153253 }
154254 return true ;
0 commit comments