@@ -43,6 +43,83 @@ def _get_device_module(device=None):
4343 return torch .cuda
4444
4545
46+ class _StagingPool :
47+ """Bounded, pre-allocated staging slots for one TransferGroup.
48+
49+ Save and load share one pool. Replaces per-task ``torch.empty``: the GPU
50+ side becomes a fixed, configurable HBM reservation instead of unbounded
51+ dynamic allocation that competes with the engine (VRAM OOM under load on
52+ low-headroom GPUs), and an exhausted pool blocks the acquiring task --
53+ backpressure -- instead of failing. Slots are handed out as *contiguous*
54+ runs because the bulk D2H/H2D copies and the kernel views need one piece
55+ of memory.
56+ """
57+
58+ def __init__ (self , device , per_block_bytes : int , max_blocks : int ):
59+ if max_blocks <= 0 :
60+ raise ValueError ("staging pool must have at least one block slot" )
61+ self .block_bytes = per_block_bytes
62+ self .max_blocks = max_blocks
63+ self ._cond = threading .Condition ()
64+ total = max_blocks * per_block_bytes
65+ # Pinned host memory needs a CUDA context; on other devices (tests,
66+ # CPU-only runs) fall back to pageable memory.
67+ self ._cpu = torch .empty (total , dtype = torch .uint8 , device = "cpu" ,
68+ pin_memory = (device .type == "cuda" ))
69+ self ._gpu = torch .empty (total , dtype = torch .uint8 , device = device )
70+ # Free runs as [start, start+len) block ranges, kept sorted by start.
71+ self ._runs = [[0 , max_blocks ]]
72+
73+ def acquire (self , n : int ) -> int :
74+ """Block until a contiguous run of ``n`` block slots is free; return
75+ its starting block index."""
76+ if n <= 0 :
77+ raise ValueError ("must acquire at least one block slot" )
78+ if n > self .max_blocks :
79+ raise ValueError (
80+ f"staging task of { n } blocks exceeds the pool capacity "
81+ f"{ self .max_blocks } ; raise staging_pool_blocks or shrink "
82+ f"block_per_save_task/block_per_load_task" )
83+ with self ._cond :
84+ while True :
85+ for i , (start , length ) in enumerate (self ._runs ):
86+ if length >= n :
87+ rest = length - n
88+ if rest :
89+ self ._runs [i ] = [start + n , rest ]
90+ else :
91+ self ._runs .pop (i )
92+ return start
93+ self ._cond .wait ()
94+
95+ def release (self , start : int , n : int ) -> None :
96+ with self ._cond :
97+ pos , run = 0 , [start , n ]
98+ for pos , (s , _len ) in enumerate (self ._runs ):
99+ if s > start :
100+ break
101+ else :
102+ pos = len (self ._runs )
103+ self ._runs .insert (pos , run )
104+ # Merge with the neighbours the release just glued together.
105+ merged = []
106+ for s , l in self ._runs :
107+ if merged and merged [- 1 ][0 ] + merged [- 1 ][1 ] == s :
108+ merged [- 1 ][1 ] += l
109+ else :
110+ merged .append ([s , l ])
111+ self ._runs = merged
112+ self ._cond .notify_all ()
113+
114+ def cpu_view (self , start : int , n : int ) -> torch .Tensor :
115+ b = self .block_bytes
116+ return self ._cpu [start * b :(start + n ) * b ]
117+
118+ def gpu_view (self , start : int , n : int ) -> torch .Tensor :
119+ b = self .block_bytes
120+ return self ._gpu [start * b :(start + n ) * b ]
121+
122+
46123class MultiResult :
47124 """Collect the per-block success flags of several async tasks and fire a
48125 callback once every task has reported. Each result is a list[bool] aligned
@@ -79,6 +156,27 @@ def __init__(self, kvcache_info: KVCacheInfo, manager_block_size: int,
79156 self ._save_stream = self ._device_mod .Stream ()
80157 self ._load_stream = self ._device_mod .Stream ()
81158
159+ pool_blocks = extra_config .staging_pool_blocks
160+ need = max (extra_config .block_per_save_task ,
161+ extra_config .block_per_load_task )
162+ if pool_blocks < need :
163+ raise ValueError (
164+ f"staging_pool_blocks={ pool_blocks } is smaller than the "
165+ f"largest task batch ({ need } ); one task stages its whole "
166+ f"batch contiguously, so the pool must cover it" )
167+ # One pool per group: block shapes differ between attention and state
168+ # groups. The GPU side is the HBM this connector permanently reserves.
169+ self ._pools = {
170+ g .spec_name : _StagingPool (self ._device , g .per_block_bytes , pool_blocks )
171+ for g in kvcache_info .groups }
172+ for name , pool in self ._pools .items ():
173+ logger .info ("staging pool %s: %d blocks x %d bytes "
174+ "(pinned %.1f MiB + GPU %.1f MiB)" ,
175+ name , pool .max_blocks ,
176+ pool .block_bytes ,
177+ pool .max_blocks * pool .block_bytes / 2 ** 20 ,
178+ pool .max_blocks * pool .block_bytes / 2 ** 20 )
179+
82180 def _init_worker ():
83181 self ._device_mod .set_device (self ._device )
84182
@@ -223,43 +321,52 @@ def _save_valid_blocks(self, group, remote_uris,
223321 assert all (uri is not None for uri in uris ), \
224322 f"group { group .spec_name } : save batch contains a block without a " \
225323 f"location; _save_dispositions must have failed it"
226- cpu_buffer = torch .empty (len (valid ) * group .per_block_bytes , dtype = torch .uint8 ,
227- device = "cpu" , pin_memory = True )
324+ pool = self ._pools [group .spec_name ]
228325 with self ._device_mod .stream (self ._save_stream ):
229326 ready_event .wait ()
230- gpu_buffer = torch .empty (len (valid ) * group .per_block_bytes ,
231- dtype = torch .uint8 , device = self ._device )
232- if isinstance (group , AttentionTransferGroup ):
233- view = gpu_buffer .view (self ._info .dtype ).view (
234- len (valid ), group .num_kv_ptrs ,
235- self ._manager_block_size , group .per_token_dim )
236- batch_gather_scatter_helper .batch_gather_kv_caches (
237- group .kvcache_ptr_tensor_gpu , view ,
238- [block_token_indices [i ] for i in valid ],
239- list (range (len (valid ))), self ._manager_block_size ,
240- group .per_token_dim ,
241- block_stride = group .block_stride ,
242- local_block_size = group .kernel_block_size )
243- else :
244- for out_i , i in enumerate (valid ):
245- for layer_idx in range (group .layer_num ):
246- dst = (out_i * group .layer_num + layer_idx ) * group .page_size_bytes
247- gpu_buffer [dst :dst + group .page_size_bytes ].copy_ (
248- group .block_view_tensors [layer_idx ][block_ids [i ]])
249- cpu_buffer .copy_ (gpu_buffer , non_blocking = True )
250- done = self ._device_mod .Event ()
251- done .record (self ._save_stream )
252- done .synchronize ()
253-
254- buffers = self ._make_block_buffers (
255- cpu_buffer .data_ptr (), group .per_block_bytes , len (valid ))
256- result = self ._transfer_client .SaveKvCaches (uris , buffers )
257- ok = (result [0 ] == kvcm_py_client .ClientErrorCode .ER_OK )
258- if not ok :
259- logger .warning ("save task failed group=%s uris=%d result=%s" ,
260- group .spec_name , len (uris ), result )
261- for i in valid :
262- ok_mask [i ] = ok
327+ start = pool .acquire (len (valid ))
328+ try :
329+ cpu_buffer = pool .cpu_view (start , len (valid ))
330+ gpu_buffer = pool .gpu_view (start , len (valid ))
331+ with self ._device_mod .stream (self ._save_stream ):
332+ if isinstance (group , AttentionTransferGroup ):
333+ view = gpu_buffer .view (self ._info .dtype ).view (
334+ len (valid ), group .num_kv_ptrs ,
335+ self ._manager_block_size , group .per_token_dim )
336+ batch_gather_scatter_helper .batch_gather_kv_caches (
337+ group .kvcache_ptr_tensor_gpu , view ,
338+ [block_token_indices [i ] for i in valid ],
339+ list (range (len (valid ))), self ._manager_block_size ,
340+ group .per_token_dim ,
341+ block_stride = group .block_stride ,
342+ local_block_size = group .kernel_block_size )
343+ else :
344+ for out_i , i in enumerate (valid ):
345+ for layer_idx in range (group .layer_num ):
346+ dst = (out_i * group .layer_num + layer_idx ) * group .page_size_bytes
347+ gpu_buffer [dst :dst + group .page_size_bytes ].copy_ (
348+ group .block_view_tensors [layer_idx ][block_ids [i ]])
349+ cpu_buffer .copy_ (gpu_buffer , non_blocking = True )
350+ done = self ._device_mod .Event ()
351+ done .record (self ._save_stream )
352+ done .synchronize ()
353+
354+ buffers = self ._make_block_buffers (
355+ cpu_buffer .data_ptr (), group .per_block_bytes , len (valid ))
356+ result = self ._transfer_client .SaveKvCaches (uris , buffers )
357+ ok = (result [0 ] == kvcm_py_client .ClientErrorCode .ER_OK )
358+ if not ok :
359+ logger .warning ("save task failed group=%s uris=%d result=%s" ,
360+ group .spec_name , len (uris ), result )
361+ for i in valid :
362+ ok_mask [i ] = ok
363+ except BaseException :
364+ # Drain the stream before the slots go back: a failed task may
365+ # have left kernel/copy work enqueued against the staging views.
366+ self ._save_stream .synchronize ()
367+ raise
368+ finally :
369+ pool .release (start , len (valid ))
263370
264371 def create_save_done_callback (self , req_id , tp_rank , write_session_id , num_blocks ):
265372 """block success = AND across all groups that had data for the block.
@@ -326,43 +433,52 @@ def load_task(self, multi_result: MultiResult, task_idx, group: TransferGroup,
326433
327434 def _load_valid_blocks (self , group , remote_uris , block_token_indices ,
328435 block_ids , valid ) -> bool :
329- cpu_buffer = torch .empty (len (valid ) * group .per_block_bytes , dtype = torch .uint8 ,
330- device = "cpu" , pin_memory = True )
331- buffers = self ._make_block_buffers (cpu_buffer .data_ptr (),
332- group .per_block_bytes , len (valid ))
333- uris = [remote_uris [i ] for i in valid ]
334- assert all (uri is not None for uri in uris ), \
335- f"group { group .spec_name } : load batch contains a block without a " \
336- f"location; load_task must have failed it"
337- result = self ._transfer_client .LoadKvCaches (uris , buffers )
338- ok = (result == kvcm_py_client .ClientErrorCode .ER_OK )
339- if ok :
340- with self ._device_mod .stream (self ._load_stream ):
341- gpu_buffer = cpu_buffer .to (self ._device , non_blocking = True )
342- if isinstance (group , AttentionTransferGroup ):
343- view = gpu_buffer .view (self ._info .dtype ).view (
344- len (valid ), group .num_kv_ptrs ,
345- self ._manager_block_size , group .per_token_dim )
346- batch_gather_scatter_helper .batch_scatter_kv_caches (
347- group .kvcache_ptr_tensor_gpu , view ,
348- [block_token_indices [i ] for i in valid ],
349- list (range (len (valid ))), self ._manager_block_size ,
350- group .per_token_dim ,
351- block_stride = group .block_stride ,
352- local_block_size = group .kernel_block_size )
353- else :
354- for out_i , i in enumerate (valid ):
355- for layer_idx in range (group .layer_num ):
356- src = (out_i * group .layer_num + layer_idx ) * group .page_size_bytes
357- group .block_view_tensors [layer_idx ][block_ids [i ]].copy_ (
358- gpu_buffer [src :src + group .page_size_bytes ])
359- done = self ._device_mod .Event ()
360- done .record (self ._load_stream )
361- done .synchronize ()
362- else :
363- logger .warning ("load task failed group=%s uris=%d result=%s" ,
364- group .spec_name , len (uris ), result )
365- return ok
436+ pool = self ._pools [group .spec_name ]
437+ start = pool .acquire (len (valid ))
438+ try :
439+ cpu_buffer = pool .cpu_view (start , len (valid ))
440+ buffers = self ._make_block_buffers (cpu_buffer .data_ptr (),
441+ group .per_block_bytes , len (valid ))
442+ uris = [remote_uris [i ] for i in valid ]
443+ assert all (uri is not None for uri in uris ), \
444+ f"group { group .spec_name } : load batch contains a block without a " \
445+ f"location; load_task must have failed it"
446+ result = self ._transfer_client .LoadKvCaches (uris , buffers )
447+ ok = (result == kvcm_py_client .ClientErrorCode .ER_OK )
448+ if ok :
449+ with self ._device_mod .stream (self ._load_stream ):
450+ gpu_buffer = pool .gpu_view (start , len (valid ))
451+ gpu_buffer .copy_ (cpu_buffer , non_blocking = True )
452+ if isinstance (group , AttentionTransferGroup ):
453+ view = gpu_buffer .view (self ._info .dtype ).view (
454+ len (valid ), group .num_kv_ptrs ,
455+ self ._manager_block_size , group .per_token_dim )
456+ batch_gather_scatter_helper .batch_scatter_kv_caches (
457+ group .kvcache_ptr_tensor_gpu , view ,
458+ [block_token_indices [i ] for i in valid ],
459+ list (range (len (valid ))), self ._manager_block_size ,
460+ group .per_token_dim ,
461+ block_stride = group .block_stride ,
462+ local_block_size = group .kernel_block_size )
463+ else :
464+ for out_i , i in enumerate (valid ):
465+ for layer_idx in range (group .layer_num ):
466+ src = (out_i * group .layer_num + layer_idx ) * group .page_size_bytes
467+ group .block_view_tensors [layer_idx ][block_ids [i ]].copy_ (
468+ gpu_buffer [src :src + group .page_size_bytes ])
469+ done = self ._device_mod .Event ()
470+ done .record (self ._load_stream )
471+ done .synchronize ()
472+ else :
473+ logger .warning ("load task failed group=%s uris=%d result=%s" ,
474+ group .spec_name , len (uris ), result )
475+ return ok
476+ except BaseException :
477+ # Drain the stream before the slots go back (as in save).
478+ self ._load_stream .synchronize ()
479+ raise
480+ finally :
481+ pool .release (start , len (valid ))
366482
367483 def create_load_done_callback (self , req_id , tp_rank , epoch , block_ids , num_blocks ,
368484 report_failures = True ):
0 commit comments