main
py 771 lines 30.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # Adapted from
4 # https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/utils/offloader.py
5 """Prefetch-based CPU offloading with async prefetching.
6
7 Uses static buffers and event-based stream forking for torch.compile +
8 CUDA graph compatibility. Events allow the copy stream to join CUDA
9 graph captures, ensuring H2D copies are properly captured.
10 """
11
12 from abc import ABC, abstractmethod
13 from collections.abc import Generator
14 from dataclasses import dataclass
15 from typing import Any
16
17 import torch
18 import torch.nn as nn
19
20 # Import prefetch_ops to register custom ops at module load time
21 import vllm.model_executor.offloader.prefetch_ops # noqa: F401
22 from vllm.logger import init_logger
23 from vllm.model_executor.offloader.base import BaseOffloader, should_pin_memory
24 from vllm.utils.torch_utils import get_dtype_size
25
26 logger = init_logger(__name__)
27
28
29 @dataclass
30 class ParamInfo:
31 """Metadata about an offloaded parameter."""
32
33 name: str
34 shape: tuple[int, ...]
35 stride: tuple[int, ...]
36 dtype: torch.dtype
37
38 @property
39 def key(self) -> tuple[str, tuple[int, ...], tuple[int, ...], torch.dtype]:
40 """Unique key for buffer pool grouping.
41
42 Includes parameter name to prevent different parameters with the same
43 shape from sharing buffers within the same layer. Parameters with the
44 same name across different layers will share buffers (via slots).
45
46 Includes stride because parameters with same shape but different
47 strides need separate buffers to preserve memory layout.
48 """
49 return (self.name, self.shape, self.stride, self.dtype)
50
51 @property
52 def num_bytes(self) -> int:
53 """Size in bytes."""
54 numel = 1
55 for dim in self.shape:
56 numel *= dim
57 return numel * get_dtype_size(self.dtype)
58
59
60 class StaticBufferPool:
61 """Pre-allocated GPU buffer pool for offloaded parameters.
62
63 Allocates slot_capacity copies of each unique parameter
64 (name, shape, stride, dtype), allowing for double/triple buffering
65 during prefetch.
66
67 Buffer slots are reused circularly: layer N uses slot (N % slot_capacity).
68
69 The key includes parameter name to prevent different parameters within
70 the same layer from sharing buffers. Parameters with the same name
71 across different layers share buffers via the slot mechanism.
72 """
73
74 def __init__(
75 self,
76 param_infos: list[ParamInfo],
77 slot_capacity: int,
78 device: torch.device,
79 ):
80 self.slot_capacity = slot_capacity
81 self.total_bytes = 0
82 self._device = device
83
84 # Group by (shape, stride, dtype) - only allocate unique combinations
85 unique_params: dict[tuple, ParamInfo] = {}
86 for info in param_infos:
87 if info.key not in unique_params:
88 unique_params[info.key] = info
89
90 # Allocate buffers: key -> list of tensors (one per slot)
91 self._buffers: dict[tuple, list[torch.Tensor]] = {}
92 for key, info in unique_params.items():
93 slot_tensors = []
94 for _ in range(slot_capacity):
95 # Use empty_strided to preserve parameter's memory layout
96 buf = torch.empty_strided(
97 size=info.shape,
98 stride=info.stride,
99 dtype=info.dtype,
100 device=device,
101 )
102 slot_tensors.append(buf)
103 self.total_bytes += info.num_bytes
104 self._buffers[key] = slot_tensors
105
106 logger.debug(
107 "[StaticBufferPool] Allocated %d unique (name, shape, stride, dtype), "
108 "%d slots each, total %.4f GB",
109 len(unique_params),
110 slot_capacity,
111 self.total_bytes / 1e9,
112 )
113
114 def get_buffer(
115 self,
116 name: str,
117 shape: tuple[int, ...],
118 stride: tuple[int, ...],
119 dtype: torch.dtype,
120 slot_idx: int,
121 ) -> torch.Tensor:
122 """Get a static buffer for the given name/shape/stride/dtype/slot."""
123 key = (name, shape, stride, dtype)
124 return self._buffers[key][slot_idx % self.slot_capacity]
125
126
127 def _get_next_prefetch_index(
128 index: int,
129 prefetch_step: int,
130 module_count: int,
131 ) -> int:
132 """Return a refill target that preserves static-buffer slot ownership."""
133 next_index = (index + prefetch_step) % module_count
134 if (
135 prefetch_step < module_count
136 and next_index % prefetch_step != index % prefetch_step
137 ):
138 next_index = index % prefetch_step
139 return next_index
140
141
142 class PrefetchOffloader(BaseOffloader):
143 """Prefetching-based offloader with group-based layer selection.
144
145 Groups layers and uses async H2D prefetch to hide transfer latency.
146 Uses static buffers and stream synchronization for torch.compile and
147 CUDA graph compatibility.
148
149 Args:
150 group_size: Group every N layers together.
151 num_in_group: Offload this many layers per group (last N of each group).
152 prefetch_step: Number of layers to prefetch ahead.
153 mode: Offload mode ("cpu" is currently supported).
154
155 """
156
157 def __init__(
158 self,
159 group_size: int,
160 num_in_group: int,
161 prefetch_step: int,
162 offload_params: set[str] | None = None,
163 mode: str = "cpu",
164 ):
165 self.group_size = group_size
166 self.num_in_group = num_in_group
167 self.prefetch_step = prefetch_step
168 self.offload_params = offload_params or set()
169 self.mode = mode
170
171 # Copy stream for async H2D transfers
172 self.copy_stream = torch.cuda.Stream()
173
174 # Module offloaders and buffer pool (populated in wrap_modules/post_init)
175 self.module_offloaders: list[_ModuleOffloader] = []
176 self.buffer_pool: StaticBufferPool | None = None
177 self.total_offloaded_bytes = 0
178
179 def wrap_modules(
180 self,
181 modules_generator: Generator[nn.Module, None, None],
182 prefix: str = "",
183 ) -> list[nn.Module]:
184 """Wrap modules with prefetch offloading logic."""
185 assert len(self.module_offloaders) == 0, (
186 "wrap_modules should only be called once"
187 )
188
189 if prefix:
190 prefix = f"{prefix}."
191
192 all_modules = []
193 offload_modules = []
194
195 for module_index, module in enumerate(modules_generator):
196 all_modules.append(module)
197
198 # Select layers to offload based on group pattern
199 # Offload last num_in_group layers of each group_size
200 if module_index % self.group_size >= self.group_size - self.num_in_group:
201 if self.offload_params:
202 whitelist = [
203 name
204 for name, _ in module.named_parameters()
205 if any(
206 f".{p}." in f".{prefix}{name}." for p in self.offload_params
207 )
208 ]
209 else:
210 whitelist = [name for name, _ in module.named_parameters()]
211
212 if not whitelist:
213 continue # skip layers with no matching params
214
215 offload_modules.append(module)
216 self.module_offloaders.append(
217 _ModuleOffloader(
218 mode=self.mode,
219 module=module,
220 copy_stream=self.copy_stream,
221 whitelist_param_names=whitelist,
222 layer_idx=len(self.module_offloaders),
223 )
224 )
225
226 for index, module in enumerate(offload_modules):
227 self._hook_module_forward(index, module)
228
229 return all_modules
230
231 def _hook_module_forward(self, index: int, module: nn.Module):
232 """Hook module's forward with torch.compile-compatible sync."""
233 original_forward = module.forward
234
235 def forward(*args, **kwargs):
236 # Temporarily restore original forward to avoid recursion
237 module.forward = original_forward
238
239 # Wait for this layer's prefetch to complete
240 # mutates_args on input_tensor creates data dependency for torch.compile
241 input_tensor = args[0] if args else kwargs.get("hidden_states")
242 torch.ops.vllm.wait_prefetch(input_tensor, index)
243
244 # No parameter swapping needed - parameters already point to
245 # GPU static buffers (set in assign_static_buffer)
246 output = original_forward(*args, **kwargs)
247
248 # Start prefetch for next layer (circular)
249 # mutates_args on output_tensor creates ordering dependency
250 next_index = _get_next_prefetch_index(
251 index,
252 self.prefetch_step,
253 len(self.module_offloaders),
254 )
255 # Handle tuple output (e.g., (hidden_states, residual))
256 if isinstance(output, tuple):
257 torch.ops.vllm.start_prefetch(output[0], next_index)
258 else:
259 torch.ops.vllm.start_prefetch(output, next_index)
260
261 # No explicit offload needed - static buffers are reused implicitly
262
263 # Restore hooked forward
264 module.forward = forward
265 return output
266
267 module.forward = forward
268
269 def _wait_for_layer(self, layer_idx: int):
270 """Called by custom op - wait for copy to complete.
271
272 Synchronization strategy:
273 - During CUDA graph capture: use event-based wait (graph-compatible)
274 - Outside capture (warmup/eager): use wait_stream (more robust)
275
276 During capture, we skip wait for pre-capture prefetches because:
277 1. sync_before_graph_capture() ensures pre-capture work is complete
278 2. We can't wait on pre-capture events during capture (isolation error)
279 """
280 offloader = self.module_offloaders[layer_idx]
281
282 if torch.cuda.is_current_stream_capturing():
283 # During capture, skip wait for pre-capture prefetches.
284 # sync_before_graph_capture() ensures pre-capture work is complete.
285 if not offloader._prefetch_in_capture:
286 return
287 # Event-based wait for in-capture prefetches (graph-compatible)
288 torch.cuda.current_stream().wait_event(offloader._copy_done_event)
289 # Mark that this prefetch has been waited on (joined).
290 offloader._prefetch_in_capture = False
291 else:
292 if offloader._event_valid_for_eager:
293 # Use per-layer event to only wait for THIS layer's copy,
294 # allowing other layers' prefetches to run concurrently.
295 torch.cuda.current_stream().wait_event(offloader._copy_done_event)
296 else:
297 # Event not usable (unrecorded or recorded during capture).
298 # Fall back to wait_stream to drain all copy_stream work.
299 torch.cuda.current_stream().wait_stream(self.copy_stream)
300
301 def sync_prev_onload(self):
302 """Sync previous onload operations.
303
304 Ensures any H2D copies in flight on copy_stream complete before
305 the compute stream continues. Call this before CUDA graph
306 capture/replay or when synchronization is needed.
307 """
308 torch.cuda.current_stream().wait_stream(self.copy_stream)
309
310 def _start_prefetch(self, layer_idx: int):
311 """Called by custom op - start async copy to static buffer."""
312 offloader = self.module_offloaders[layer_idx]
313 offloader.start_onload_to_static()
314
315 def join_after_forward(self):
316 """Join copy_stream after model forward completes.
317
318 Call this after the model forward pass but before CUDA graph capture
319 ends. This ensures copy_stream is rejoined for any prefetches started
320 during the forward pass.
321
322 We join ALL layers that have _prefetch_in_capture=True, meaning their
323 prefetch was started during capture but not yet waited on (joined).
324 This handles both full and piecewise cudagraph modes correctly:
325 - Full mode: joins layers 0..prefetch_step-1 (prefetched by last layers)
326 - Piecewise mode: joins only layers prefetched by THIS subgraph's layers
327 """
328 if not self.module_offloaders:
329 return
330 # Join all layers whose prefetch was started in capture but not waited on
331 for offloader in self.module_offloaders:
332 if offloader._prefetch_in_capture:
333 torch.cuda.current_stream().wait_event(offloader._copy_done_event)
334 offloader._prefetch_in_capture = False
335
336 def post_init(self):
337 """Allocate static buffer pool and start initial prefetches.
338
339 Note: Parameters have already been offloaded to CPU during wrap_modules()
340 (in _CpuParamOffloader.__init__), so GPU memory is available for the
341 static buffer pool.
342 """
343 # Sync CPU storage with current param.data BEFORE collecting param info.
344 # This is needed because process_weights_after_loading may have:
345 # 1. Transformed weights (quantization, transpose, etc.)
346 # 2. Created new CPU tensors via device_loading_context
347 # Our _cpu_storage would be stale otherwise.
348 for offloader in self.module_offloaders:
349 offloader.sync_cpu_storage()
350
351 # Collect parameter info (now using synced CPU storage)
352 param_infos: list[ParamInfo] = []
353 device: torch.device | None = None
354
355 for offloader in self.module_offloaders:
356 param_infos.extend(offloader.get_param_infos())
357 if device is None:
358 device = offloader.device
359
360 if device is None:
361 # No modules to offload
362 return
363
364 # Allocate static buffer pool
365 self.buffer_pool = StaticBufferPool(
366 param_infos=param_infos,
367 slot_capacity=self.prefetch_step,
368 device=device,
369 )
370
371 # Assign buffer slots and point parameters to GPU buffers
372 for idx, offloader in enumerate(self.module_offloaders):
373 slot_idx = idx % self.prefetch_step
374 offloader.assign_buffer_slot(self.buffer_pool, slot_idx)
375
376 # Collect offloaded bytes
377 for offloader in self.module_offloaders:
378 offloader.post_init()
379 self.total_offloaded_bytes += offloader.offloaded_bytes
380
381 logger.info_once(
382 f"[PrefetchOffloader] Initialized {len(self.module_offloaders)} modules. "
383 f"Total GPU memory saved: {self.total_offloaded_bytes / 1e9:.4f} GB, "
384 f"Static buffer pool: {self.buffer_pool.total_bytes / 1e9:.4f} GB "
385 f"(group_size={self.group_size}, num_in_group={self.num_in_group}, "
386 f"prefetch_step={self.prefetch_step}, mode={self.mode})"
387 )
388
389 # Start initial prefetches
390 for i in range(min(self.prefetch_step, len(self.module_offloaders))):
391 self.module_offloaders[i].start_onload_to_static()
392
393
394 class _ModuleOffloader:
395 """Manages offloading for a single module.
396
397 Uses static buffers from a shared pool instead of dynamic allocation.
398 """
399
400 def __init__(
401 self,
402 mode: str,
403 module: nn.Module,
404 copy_stream: torch.cuda.Stream,
405 whitelist_param_names: list[str],
406 layer_idx: int,
407 ):
408 self.mode = mode
409 self.module = module
410 self.device = next(module.parameters()).device
411 self.copy_stream = copy_stream
412 self.layer_idx = layer_idx
413 self.offloaded_bytes = 0
414
415 # Event to signal when H2D copy to static buffer is complete.
416 # Used for per-layer synchronization (both eager and capture modes).
417 self._copy_done_event = torch.cuda.Event()
418
419 # Track whether _copy_done_event is valid for eager-mode wait_event.
420 # False when: (1) never recorded, or (2) last recorded during a
421 # cudagraph capture (events become invalid after capture ends).
422 # In these cases we fall back to wait_stream.
423 self._event_valid_for_eager = False
424
425 # Track if last prefetch was started during CUDA graph capture.
426 # Used to skip wait_event during capture for pre-capture prefetches.
427 self._prefetch_in_capture = False
428
429 assert self.device != torch.device("cpu"), (
430 "Module parameters should not already be on CPU "
431 "(offloader handles CPU placement)"
432 )
433
434 # Buffer pool and slot (assigned in assign_buffer_slot)
435 self._buffer_pool: StaticBufferPool | None = None
436 self._buffer_slot_idx: int = 0
437
438 param_dict = dict(self.module.named_parameters())
439 assert all(name in param_dict for name in whitelist_param_names), (
440 f"Whitelist params {whitelist_param_names} not found in module params "
441 f"{list(param_dict.keys())}"
442 )
443
444 self._param_offloaders = {
445 name: _BaseParamOffloader.create(mode, module=module, param_name=name)
446 for name in whitelist_param_names
447 }
448
449 def post_init(self):
450 """Collect total offloaded bytes (offloading already done in __init__)."""
451 for param_offloader in self._param_offloaders.values():
452 param_offloader.post_init()
453 self.offloaded_bytes += param_offloader.offloaded_bytes
454
455 def sync_cpu_storage(self):
456 """Sync CPU storage with current param.data.
457
458 Called after process_weights_after_loading to ensure _cpu_storage
459 contains the final processed weights, not stale pre-loading data.
460
461 Parameters whose underlying nn.Parameter was deleted by
462 process_weights_after_loading (e.g. transient KV-cache scale params)
463 are pruned from self._param_offloaders so they do not participate in
464 buffer-pool allocation or prefetching.
465 """
466 for param_offloader in self._param_offloaders.values():
467 param_offloader.sync_cpu_storage()
468
469 # Remove offloaders whose parameter was deleted during
470 # process_weights_after_loading (e.g. k_scale / v_scale).
471 deleted = [
472 name
473 for name, offloader in self._param_offloaders.items()
474 if getattr(offloader, "_param_deleted", False)
475 ]
476 if deleted:
477 logger.debug(
478 "Pruning %d transient offloaded param(s) that were deleted "
479 "by process_weights_after_loading: %s",
480 len(deleted),
481 deleted,
482 )
483 for name in deleted:
484 del self._param_offloaders[name]
485
486 def get_param_infos(self) -> list[ParamInfo]:
487 """Get parameter metadata for buffer pool allocation.
488
489 Note: sync_cpu_storage() must be called before this method to ensure
490 _cpu_storage reflects the final processed weights (after quantization).
491 """
492 infos = []
493 for name, offloader in self._param_offloaders.items():
494 cpu_storage = offloader._cpu_storage
495 assert cpu_storage is not None, "CPU storage not initialized"
496 infos.append(
497 ParamInfo(
498 name=name,
499 shape=tuple(cpu_storage.shape),
500 stride=tuple(cpu_storage.stride()),
501 dtype=cpu_storage.dtype,
502 )
503 )
504 return infos
505
506 def assign_buffer_slot(self, pool: StaticBufferPool, slot_idx: int):
507 """Assign this module to a buffer slot in the pool.
508
509 Also assigns static GPU buffers to each parameter offloader,
510 which moves the parameter data to point to the GPU buffer.
511 """
512 self._buffer_pool = pool
513 self._buffer_slot_idx = slot_idx
514
515 # Assign static buffers to parameters
516 # Use CPU storage shape/stride/dtype since param.data is now empty
517 for name, offloader in self._param_offloaders.items():
518 cpu_storage = offloader._cpu_storage
519 assert cpu_storage is not None, "CPU storage not initialized"
520 buffer = pool.get_buffer(
521 name=name,
522 shape=tuple(cpu_storage.shape),
523 stride=tuple(cpu_storage.stride()),
524 dtype=cpu_storage.dtype,
525 slot_idx=slot_idx,
526 )
527 offloader.assign_static_buffer(buffer)
528
529 def start_onload_to_static(self):
530 """Start async copy from CPU storage to GPU buffer.
531
532 Uses event-based forking to join copy_stream to CUDA graph capture.
533 This ensures H2D copies are properly captured when recording a graph.
534
535 IMPORTANT: We must wait for the compute stream before copying, because
536 the previous layer's forward may still be using the buffer (GPU ops are
537 async). Without this sync, we could overwrite the buffer while it's
538 being read.
539 """
540 assert self._buffer_pool is not None, "Buffer pool not assigned"
541
542 # Track if this prefetch is being captured (for _wait_for_layer logic)
543 self._prefetch_in_capture = torch.cuda.is_current_stream_capturing()
544
545 # Fork: record event on compute stream, copy_stream waits on it
546 # This joins copy_stream to any active CUDA graph capture
547 fork_event = torch.cuda.Event()
548 torch.cuda.current_stream().record_event(fork_event)
549 self.copy_stream.wait_event(fork_event)
550
551 with torch.cuda.stream(self.copy_stream):
552 for name, offloader in self._param_offloaders.items():
553 cpu_storage = offloader._cpu_storage
554 gpu_buffer = offloader._gpu_buffer
555 assert cpu_storage is not None, "CPU storage not initialized"
556 assert gpu_buffer is not None, "GPU buffer not assigned"
557 assert not should_pin_memory() or cpu_storage.is_pinned(), (
558 f"CPU storage for {name} is not pinned! "
559 "non_blocking=True H2D copy from non-pinned memory "
560 "causes stream synchronization that breaks "
561 "event-based fork synchronization."
562 )
563 gpu_buffer.copy_(cpu_storage, non_blocking=True)
564
565 # Record completion event for _wait_for_layer to use
566 self._copy_done_event.record(self.copy_stream)
567 # Event is only valid for eager wait_event if recorded outside capture.
568 # Events recorded during capture become invalid after capture ends.
569 self._event_valid_for_eager = not torch.cuda.is_current_stream_capturing()
570
571
572 class _BaseParamOffloader(ABC):
573 """Base class for parameter offloading strategies."""
574
575 # CPU storage for offloaded parameters (set by subclasses)
576 _cpu_storage: torch.Tensor | None
577 # GPU buffer reference (set by subclasses when using static buffers)
578 _gpu_buffer: torch.Tensor | None
579
580 @staticmethod
581 def create(mode: str, **kwargs) -> "_BaseParamOffloader":
582 """Factory method to create appropriate offloader for mode."""
583 if mode == "cpu":
584 return _CpuParamOffloader(**kwargs)
585 else:
586 raise ValueError(f"Unknown offload mode: {mode}")
587
588 def __init__(self, module: nn.Module, param_name: str):
589 self._module = module
590 self._param_name = param_name
591 self.offloaded_bytes = 0
592 self._cpu_storage = None
593 self._gpu_buffer = None
594
595 @property
596 def _param(self) -> nn.Parameter:
597 """Get the parameter being offloaded.
598
599 Supports dotted names (e.g. 'self_attn.qkv_proj.weight') by
600 traversing the module hierarchy.
601 """
602 obj: Any = self._module
603 for attr in self._param_name.split("."):
604 obj = getattr(obj, attr)
605 return obj
606
607 def post_init(self):
608 """Initialize offloading (move parameter to storage)."""
609 return
610
611 @abstractmethod
612 def sync_cpu_storage(self) -> None:
613 """Sync CPU storage with current param.data.
614
615 Called after process_weights_after_loading to update _cpu_storage
616 with the final processed weights.
617 """
618 pass
619
620 @abstractmethod
621 def assign_static_buffer(self, gpu_buffer: torch.Tensor) -> None:
622 """Point parameter data to GPU static buffer."""
623 pass
624
625
626 class _CpuParamOffloader(_BaseParamOffloader):
627 """Offload parameter to pinned CPU memory.
628
629 Uses GPU static buffers as the actual parameter, with CPU storage
630 kept separately. This ensures torch.compile sees GPU tensors at trace time.
631
632 The offloading happens in two phases:
633 1. __init__() - copies GPU data to CPU, frees GPU memory immediately
634 2. assign_static_buffer() - points param.data to GPU static buffer
635 """
636
637 def __init__(self, module: nn.Module, param_name: str):
638 super().__init__(module, param_name)
639 self._cpu_storage: torch.Tensor | None = None
640 self._gpu_buffer: torch.Tensor | None = None # Store reference to GPU buffer
641 # Set to True if the underlying nn.Parameter was deleted by
642 # process_weights_after_loading (e.g. transient KV-cache scale params
643 # such as k_scale/v_scale created by BaseKVCacheMethod.create_weights
644 # and deleted after copying into permanent _k_scale buffers).
645 self._param_deleted: bool = False
646
647 # Offload to CPU immediately to free GPU memory during model loading
648 self._offload_to_cpu_internal()
649
650 def _offload_to_cpu_internal(self):
651 """Copy parameter data to pinned CPU storage and free GPU memory.
652
653 This replaces param.data with CPU storage, allowing weight loading
654 to continue writing to CPU memory. GPU memory is freed when the
655 original GPU tensor is garbage collected.
656 """
657 param = self._param
658 pin_memory = should_pin_memory()
659
660 # Create pinned CPU storage and copy current GPU data
661 self._cpu_storage = torch.empty_strided(
662 size=param.data.size(),
663 stride=param.data.stride(),
664 dtype=param.data.dtype,
665 layout=param.data.layout,
666 device="cpu",
667 pin_memory=pin_memory,
668 )
669 self._cpu_storage.copy_(param.data)
670
671 self.offloaded_bytes = (
672 self._cpu_storage.numel() * self._cpu_storage.element_size()
673 )
674
675 # Point param.data to CPU storage - this allows weight loading to work
676 # and frees GPU memory when the original GPU tensor is garbage collected
677 param.data = self._cpu_storage
678
679 def _update_cpu_storage_from_param(self) -> None:
680 """Update _cpu_storage from current param.data, ensuring pinned memory.
681
682 After process_weights_after_loading, device_loading_context creates
683 non-pinned CPU tensors via `p.data = p.data.to("cpu")`. Using
684 non-pinned memory with `copy_(src, non_blocking=True)` causes CUDA to
685 perform a stream synchronization before the copy, breaking the
686 event-based fork synchronization and potentially allowing the copy
687 to overwrite the GPU buffer while the compute stream still reads it.
688
689 This method ensures _cpu_storage always uses pinned memory when
690 available, re-pinning if necessary.
691 """
692 param = self._param
693
694 if param.data.device.type == "cpu":
695 if should_pin_memory() and not param.data.is_pinned():
696 pinned = torch.empty_strided(
697 size=param.data.size(),
698 stride=param.data.stride(),
699 dtype=param.data.dtype,
700 layout=param.data.layout,
701 device="cpu",
702 pin_memory=True,
703 )
704 pinned.copy_(param.data)
705 self._cpu_storage = pinned
706 else:
707 self._cpu_storage = param.data
708 else:
709 # param.data is on GPU - copy to existing CPU storage
710 assert self._cpu_storage is not None
711 self._cpu_storage.copy_(param.data)
712
713 def assign_static_buffer(self, gpu_buffer: torch.Tensor) -> None:
714 """Point parameter data to GPU static buffer.
715
716 This is called after weight loading AND process_weights_after_loading
717 complete. At this point:
718 - param.data may have been replaced by device_loading_context
719 (which creates new CPU tensors after quantization processing)
720 - We need to update _cpu_storage to point to current param.data
721 so that prefetch copies the processed weights, not stale data
722 - Then point param.data to the GPU buffer for torch.compile
723 """
724 assert self._cpu_storage is not None, (
725 "_offload_to_cpu_internal() must be called before assign_static_buffer()"
726 )
727
728 # Get current parameter (may have been replaced by
729 # process_weights_after_loading)
730 param = self._param
731
732 # Update _cpu_storage to current param.data. This is critical because:
733 # 1. process_weights_after_loading may transform weights (quantization)
734 # 2. device_loading_context creates NEW CPU tensors when moving back
735 # 3. Our old _cpu_storage would have pre-processed or stale data
736 self._update_cpu_storage_from_param()
737
738 # Store reference to GPU buffer for use in start_onload
739 self._gpu_buffer = gpu_buffer
740
741 # Point parameter to static GPU buffer - this is what torch.compile sees
742 param.data = gpu_buffer
743
744 def sync_cpu_storage(self) -> None:
745 """Sync CPU storage with current param.data.
746
747 Called after process_weights_after_loading to update _cpu_storage
748 with the final processed weights. This is critical because:
749 1. process_weights_after_loading may transform weights (quantization)
750 2. device_loading_context creates NEW CPU tensors when moving back
751 3. Our old _cpu_storage would have pre-processed or stale data
752
753 If the parameter no longer exists on the module (e.g. transient
754 KV-cache scale parameters such as k_scale/v_scale that are created
755 by BaseKVCacheMethod.create_weights() and then deleted by
756 process_weights_after_loading() after copying their values into
757 permanent _k_scale buffers), the offloader marks itself as deleted
758 and skips the sync. The caller (_ModuleOffloader.sync_cpu_storage)
759 is responsible for removing these stale entries.
760 """
761 try:
762 self._update_cpu_storage_from_param()
763 except AttributeError:
764 # The parameter was deleted by process_weights_after_loading.
765 # Drop the now-stale CPU storage so this offloader can be pruned.
766 self._param_deleted = True
767 self._cpu_storage = None
768
769 def post_init(self):
770 """No-op: offloading done in offload_to_cpu/assign_static_buffer."""
771 pass