2727DEVICE_TYPE = current_platform .device_type
2828DEVICES = [f"{ DEVICE_TYPE } :0" ]
2929NUM_MAPPINGS = [3 ]
30+ NUM_MAPPINGS_PER_GROUP = [2 ]
3031
3132
3233@pytest .mark .parametrize ("gpu_to_cpu" , [True , False ])
@@ -58,9 +59,7 @@ def test_transfer(
5859 # build CanonicalKVCacheTensor list: one per tensor
5960 kv_cache_tensors : list [CanonicalKVCacheTensor ] = []
6061 for i in range (num_tensors ):
61- gpu_tensor = torch .randint (
62- - 128 ,
63- 127 ,
62+ gpu_tensor = torch .zeros (
6463 (num_gpu_blocks , gpu_page_size_bytes ),
6564 dtype = torch .int8 ,
6665 device = device ,
@@ -119,34 +118,44 @@ def test_transfer(
119118 for j in range (block_size_factor )
120119 ]
121120
122- # maybe skip some GPU blocks to test reading from the middle of a CPU block
123- if not gpu_to_cpu :
124- blocks_to_skip = block_size_factor - 1
121+ # maybe skip some GPU blocks to test reading/writing from the middle of a CPU block
122+ blocks_to_skip = block_size_factor - 1
123+ if blocks_to_skip > 0 :
125124 gpu_blocks = gpu_blocks [blocks_to_skip :]
126125 cpu_blocks_expanded = cpu_blocks_expanded [blocks_to_skip :]
127126
128127 # set transfer direction
129128 if gpu_to_cpu :
130129 handler = handlers .gpu_to_cpu_handler
131- src_spec = GPULoadStoreSpec (gpu_blocks , group_sizes = (len (gpu_blocks ),))
130+ src_spec = GPULoadStoreSpec (
131+ gpu_blocks , group_sizes = (len (gpu_blocks ),), block_indices = (blocks_to_skip ,)
132+ )
132133 dst_spec = CPULoadStoreSpec (cpu_blocks )
133134 dst_to_src = dict (zip (cpu_blocks_expanded , gpu_blocks ))
134- num_dst_sub_blocks = num_cpu_blocks * block_size_factor
135+ num_dst_sub_blocks = num_gpu_blocks
135136 else :
136137 handler = handlers .cpu_to_gpu_handler
137138 src_spec = CPULoadStoreSpec (cpu_blocks )
138- dst_spec = GPULoadStoreSpec (gpu_blocks , group_sizes = (len (gpu_blocks ),))
139+ dst_spec = GPULoadStoreSpec (
140+ gpu_blocks , group_sizes = (len (gpu_blocks ),), block_indices = (blocks_to_skip ,)
141+ )
139142 dst_to_src = dict (zip (gpu_blocks , cpu_blocks_expanded ))
140143 num_dst_sub_blocks = num_gpu_blocks
141144
145+ # randomize src and dst tensors before transfer
146+ for tensor in handler .src_tensors :
147+ tensor .random_ ()
148+ for tensor in handler .dst_tensors :
149+ tensor .random_ ()
150+
142151 # clone src and dst tensors before transfer
143152 orig_src_tensors = [x .clone () for x in handler .src_tensors ]
144153 orig_dst_tensors = [x .clone () for x in handler .dst_tensors ]
145154
146155 # call transfer function
147156 start_time = time .time ()
148157 assert handler .transfer_async (1 , (src_spec , dst_spec ))
149- assert set ( {x .job_id for x in handler ._transfers }) == {1 }
158+ assert {x .job_id for x in handler ._transfers } == {1 }
150159
151160 # wait for transfer to complete
152161 end_time = time .time () + 10
@@ -155,11 +164,14 @@ def test_transfer(
155164 if finished :
156165 assert finished [0 ].job_id == 1
157166 assert finished [0 ].success
158- assert finished [0 ].transfer_type == (
159- ("GPU" , "CPU" ) if gpu_to_cpu else ("CPU" , "GPU" )
167+ assert (
168+ finished [0 ].transfer_type == ("GPU" , "CPU" )
169+ if gpu_to_cpu
170+ else ("CPU" , "GPU" )
160171 )
161172 assert finished [0 ].transfer_size == (
162- len (gpu_blocks ) * handler .group_block_size_in_bytes [0 ]
173+ len (gpu_blocks )
174+ * sum ([x .page_size_bytes for x in handler .kv_cache_groups_data_refs [0 ]])
163175 )
164176 assert finished [0 ].transfer_time > 0
165177 assert finished [0 ].transfer_time < (time .time () - start_time )
@@ -196,3 +208,211 @@ def test_transfer(
196208 handlers .gpu_to_cpu_handler .shutdown ()
197209 if mmap_region :
198210 mmap_region .cleanup ()
211+
212+
213+ @pytest .mark .parametrize ("gpu_to_cpu" , [True , False ])
214+ @pytest .mark .parametrize ("num_mappings_per_group" , NUM_MAPPINGS_PER_GROUP )
215+ @pytest .mark .parametrize ("gpu_page_size_bytes" , GPU_PAGE_SIZES )
216+ @pytest .mark .parametrize ("block_size_factor" , BLOCK_SIZE_FACTORS )
217+ @pytest .mark .parametrize ("num_gpu_blocks" , NUM_GPU_BLOCKS )
218+ @pytest .mark .parametrize ("num_cpu_blocks" , NUM_CPU_BLOCKS )
219+ @pytest .mark .parametrize ("seed" , SEEDS )
220+ @pytest .mark .parametrize ("device" , DEVICES )
221+ @torch .inference_mode ()
222+ def test_transfer_multi_group (
223+ default_vllm_config ,
224+ gpu_to_cpu : bool ,
225+ num_mappings_per_group : int ,
226+ gpu_page_size_bytes : int ,
227+ block_size_factor : int ,
228+ num_gpu_blocks : int ,
229+ num_cpu_blocks : int ,
230+ seed : int ,
231+ device : str ,
232+ ) -> None :
233+ """Test transfers with three KV cache groups:
234+ - Group 0: aligned transfer with num_mappings_per_group blocks
235+ - Group 1: zero blocks (empty group)
236+ - Group 2: unaligned CPU->GPU transfer (logical_offset=block_size_factor-1,
237+ causing the implementation to skip source sub-blocks) with
238+ num_mappings_per_group blocks
239+ """
240+ set_random_seed (seed )
241+
242+ # 3 groups, each with 2 tensors
243+ num_groups = 3
244+ tensors_per_group = 2
245+ num_tensors = num_groups * tensors_per_group
246+ kv_cache_tensors : list [CanonicalKVCacheTensor ] = []
247+ for _ in range (num_tensors ):
248+ gpu_tensor = torch .zeros (
249+ (num_gpu_blocks , gpu_page_size_bytes ),
250+ dtype = torch .int8 ,
251+ device = device ,
252+ )
253+ kv_cache_tensors .append (
254+ CanonicalKVCacheTensor (
255+ tensor = gpu_tensor ,
256+ page_size_bytes = gpu_page_size_bytes ,
257+ )
258+ )
259+
260+ kv_cache_groups_data_refs : list [list [CanonicalKVCacheRef ]] = [
261+ [
262+ CanonicalKVCacheRef (
263+ tensor_idx = g * tensors_per_group + i ,
264+ page_size_bytes = gpu_page_size_bytes ,
265+ )
266+ for i in range (tensors_per_group )
267+ ]
268+ for g in range (num_groups )
269+ ]
270+
271+ canonical_kv_caches = CanonicalKVCaches (
272+ tensors = kv_cache_tensors , group_data_refs = kv_cache_groups_data_refs
273+ )
274+
275+ handlers = CpuGpuOffloadingHandlers (
276+ kv_caches = canonical_kv_caches ,
277+ block_size_factor = block_size_factor ,
278+ num_cpu_blocks = num_cpu_blocks ,
279+ )
280+
281+ # group 0: aligned, group 1: empty, group 2: unaligned on CPU->GPU
282+ group_sizes_in_cpu_blocks = [num_mappings_per_group , 0 , num_mappings_per_group ]
283+
284+ total_cpu_blocks = sum (group_sizes_in_cpu_blocks )
285+ total_gpu_blocks_needed = total_cpu_blocks * block_size_factor
286+ gpu_blocks_all = random .sample (range (num_gpu_blocks ), total_gpu_blocks_needed )
287+ cpu_blocks_all = random .sample (range (num_cpu_blocks ), total_cpu_blocks )
288+
289+ # split gpu/cpu blocks per group
290+ gpu_blocks_per_group : list [list [int ]] = []
291+ cpu_blocks_per_group : list [list [int ]] = []
292+ gpu_offset = 0
293+ cpu_offset = 0
294+ for size in group_sizes_in_cpu_blocks :
295+ gpu_count = size * block_size_factor
296+ gpu_blocks_per_group .append (gpu_blocks_all [gpu_offset : gpu_offset + gpu_count ])
297+ cpu_blocks_per_group .append (cpu_blocks_all [cpu_offset : cpu_offset + size ])
298+ gpu_offset += gpu_count
299+ cpu_offset += size
300+
301+ # expand cpu blocks to gpu-page granularity
302+ cpu_blocks_expanded_per_group = [
303+ [
304+ cpu_block * block_size_factor + j
305+ for cpu_block in cpu_blocks
306+ for j in range (block_size_factor )
307+ ]
308+ for cpu_blocks in cpu_blocks_per_group
309+ ]
310+
311+ # skip sub-blocks from group 2 to test unaligned transfers.
312+ sub_blocks_to_skip = block_size_factor - 1 # e.g. 2 when block_size_factor=3
313+ if sub_blocks_to_skip > 0 :
314+ gpu_blocks_per_group [2 ] = gpu_blocks_per_group [2 ][
315+ sub_blocks_to_skip :- sub_blocks_to_skip
316+ ]
317+ cpu_blocks_expanded_per_group [2 ] = cpu_blocks_expanded_per_group [2 ][
318+ sub_blocks_to_skip :- sub_blocks_to_skip
319+ ]
320+
321+ # build flat gpu_blocks list and group_sizes in GPU blocks
322+ gpu_blocks : list [int ] = []
323+ group_sizes : list [int ] = []
324+ for gpu_blks in gpu_blocks_per_group :
325+ gpu_blocks .extend (gpu_blks )
326+ group_sizes .append (len (gpu_blks ))
327+
328+ # build flat cpu_blocks list
329+ cpu_blocks = []
330+ for cpu_blks in cpu_blocks_per_group :
331+ cpu_blocks .extend (cpu_blks )
332+
333+ # block_indices: only relevant for unaligned transfers
334+ block_indices : list [int ] = [0 , 0 , sub_blocks_to_skip ]
335+
336+ if gpu_to_cpu :
337+ handler = handlers .gpu_to_cpu_handler
338+ src_spec = GPULoadStoreSpec (
339+ gpu_blocks , group_sizes = group_sizes , block_indices = block_indices
340+ )
341+ dst_spec = CPULoadStoreSpec (cpu_blocks )
342+ # per-group mapping: cpu sub-block -> gpu sub-block
343+ dst_to_src_per_group = [
344+ dict (zip (expanded , gpu_blks ))
345+ for expanded , gpu_blks in zip (
346+ cpu_blocks_expanded_per_group , gpu_blocks_per_group
347+ )
348+ ]
349+ num_dst_sub_blocks = num_cpu_blocks * block_size_factor
350+ else :
351+ handler = handlers .cpu_to_gpu_handler
352+ src_spec = CPULoadStoreSpec (cpu_blocks )
353+ dst_spec = GPULoadStoreSpec (
354+ gpu_blocks , group_sizes = group_sizes , block_indices = block_indices
355+ )
356+ # per-group mapping: gpu sub-block -> cpu sub-block
357+ dst_to_src_per_group = [
358+ dict (zip (gpu_blks , expanded ))
359+ for gpu_blks , expanded in zip (
360+ gpu_blocks_per_group , cpu_blocks_expanded_per_group
361+ )
362+ ]
363+ num_dst_sub_blocks = num_gpu_blocks
364+
365+ # randomize src and dst tensors before transfer
366+ for tensor in handler .src_tensors :
367+ tensor .random_ ()
368+ for tensor in handler .dst_tensors :
369+ tensor .random_ ()
370+
371+ orig_src_tensors = [x .clone () for x in handler .src_tensors ]
372+ orig_dst_tensors = [x .clone () for x in handler .dst_tensors ]
373+
374+ assert handler .transfer_async (1 , (src_spec , dst_spec ))
375+ assert {x .job_id for x in handler ._transfers } == {1 }
376+
377+ end_time = time .time () + 10
378+ while time .time () < end_time :
379+ finished = handler .get_finished ()
380+ if finished :
381+ assert finished [0 ].job_id == 1
382+ assert finished [0 ].success
383+ expected_bytes = sum (
384+ group_size * sum ([x .page_size_bytes for x in data_refs ])
385+ for group_size , data_refs in zip (
386+ group_sizes , handler .kv_cache_groups_data_refs
387+ )
388+ )
389+ assert finished [0 ].transfer_size == expected_bytes
390+ break
391+ time .sleep (0.1 )
392+
393+ # verify src tensors did not change
394+ for orig_tensor , tensor in zip (orig_src_tensors , handler .src_tensors ):
395+ assert torch .equal (orig_tensor , tensor )
396+
397+ # verify dst tensors at gpu-page granularity
398+ for group_idx , dst_to_src in enumerate (dst_to_src_per_group ):
399+ group_tensor_offset = group_idx * tensors_per_group
400+ for tensor_idx in range (tensors_per_group ):
401+ src_tensor = handler .src_tensors [group_tensor_offset + tensor_idx ]
402+ dst_tensor = handler .dst_tensors [group_tensor_offset + tensor_idx ]
403+ orig_dst_tensor = orig_dst_tensors [group_tensor_offset + tensor_idx ]
404+ src_view = src_tensor .view (- 1 , gpu_page_size_bytes )
405+ dst_view = dst_tensor .view (- 1 , gpu_page_size_bytes )
406+ orig_dst_view = orig_dst_tensor .view (- 1 , gpu_page_size_bytes )
407+ for dst_sub_block in range (num_dst_sub_blocks ):
408+ src_sub_block = dst_to_src .get (dst_sub_block )
409+ if src_sub_block is not None :
410+ expected = src_view [src_sub_block ]
411+ else :
412+ expected = orig_dst_view [dst_sub_block ]
413+ torch .testing .assert_close (
414+ dst_view [dst_sub_block ].cpu (), expected .cpu ()
415+ )
416+
417+ handlers .cpu_to_gpu_handler .shutdown ()
418+ handlers .gpu_to_cpu_handler .shutdown ()
0 commit comments