main
py 212 lines 8.05 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import collections
5 import glob
6 import os
7 import time
8 from collections.abc import Generator
9 from copy import copy
10 from typing import Any
11
12 import torch
13 from torch import nn
14
15 from vllm.config import ModelConfig
16 from vllm.config.load import LoadConfig
17 from vllm.logger import init_logger
18 from vllm.model_executor.model_loader.base_loader import BaseModelLoader
19 from vllm.model_executor.model_loader.weight_utils import (
20 download_weights_from_hf,
21 runai_safetensors_weights_iterator,
22 )
23 from vllm.transformers_utils.s3_utils import glob as s3_glob
24 from vllm.transformers_utils.utils import is_s3
25
26 logger = init_logger(__name__)
27
28
29 class ShardedStateLoader(BaseModelLoader):
30 """Model loader that directly loads each worker's model state dict, which
31 enables a fast load path for large tensor-parallel models where each worker
32 only needs to read its own shard rather than the entire checkpoint. See
33 `examples/features/sharded_state/save_sharded_state_offline.py` for creating
34 a sharded checkpoint.
35 """
36
37 DEFAULT_PATTERN = "model-rank-{rank}-part-{part}.safetensors"
38
39 def __init__(self, load_config: LoadConfig):
40 super().__init__(load_config)
41
42 extra_config = (
43 {}
44 if load_config.model_loader_extra_config is None
45 else copy(load_config.model_loader_extra_config)
46 )
47 self.pattern = extra_config.pop("pattern", self.DEFAULT_PATTERN)
48 if extra_config:
49 raise ValueError(
50 f"Unexpected extra config keys for load format "
51 f"{load_config.load_format}: "
52 f"{load_config.model_loader_extra_config.keys()}"
53 )
54
55 @staticmethod
56 def _filter_subtensors(
57 tensors: dict[str, torch.Tensor],
58 ) -> dict[str, torch.Tensor]:
59 """Filter out all tensors that share the same memory or a subset of the
60 memory of another tensor.
61 """
62 same_storage_groups: dict[Any, list[tuple[str, torch.Tensor]]] = (
63 collections.defaultdict(list)
64 )
65 for key, tensor in tensors.items():
66 if tensor.numel():
67 ptr = tensor.untyped_storage().data_ptr()
68 same_storage_groups[tensor.device, ptr].append((key, tensor))
69
70 def get_end_ptr(tensor: torch.Tensor) -> int:
71 return tensor.view(-1)[-1].data_ptr() + tensor.element_size()
72
73 result: dict[str, torch.Tensor] = {}
74 for group in same_storage_groups.values():
75 for k, t in group:
76 a, b = t.data_ptr(), get_end_ptr(t)
77 for k2, t2 in group:
78 if not t2.is_contiguous():
79 continue
80 a2, b2 = t2.data_ptr(), get_end_ptr(t2)
81 if a < a2 or b2 < b:
82 continue
83 if a2 < a or b < b2 or not t.is_contiguous():
84 break # t2 covers strictly more memory than t.
85 if k2 < k:
86 # Same tensors, keep the one with the smaller key.
87 break
88 else:
89 result[k] = t
90 return result
91
92 def _prepare_weights(self, model_name_or_path: str, revision: str | None):
93 if is_s3(model_name_or_path) or os.path.isdir(model_name_or_path):
94 return model_name_or_path
95 else:
96 allow_patterns = ["*.safetensors"]
97 return download_weights_from_hf(
98 model_name_or_path,
99 self.load_config.download_dir,
100 allow_patterns,
101 revision,
102 ignore_patterns=self.load_config.ignore_patterns,
103 )
104
105 def download_model(self, model_config: ModelConfig) -> None:
106 self._prepare_weights(model_config.model, model_config.revision)
107
108 def load_weights(self, model: nn.Module, model_config: ModelConfig) -> None:
109 from vllm.distributed import get_tensor_model_parallel_rank
110
111 model_weights = model_config.model
112 if model_weights_override := model_config.model_weights:
113 model_weights = model_weights_override
114 local_model_path = model_weights
115
116 rank = get_tensor_model_parallel_rank()
117 pattern = os.path.join(
118 local_model_path,
119 self.pattern.format(rank=rank, part="*"),
120 )
121
122 filepaths = []
123 if is_s3(local_model_path):
124 file_pattern = f"*{self.pattern.format(rank=rank, part='*')}"
125 filepaths = s3_glob(path=local_model_path, allow_pattern=[file_pattern])
126 else:
127 filepaths = glob.glob(pattern)
128 if not filepaths:
129 # TODO: support un-sharded checkpoints too
130 raise ValueError(
131 f"Could not find checkpoint files '{pattern}', only "
132 f"pre-sharded checkpoints are currently supported!"
133 )
134 state_dict = self._filter_subtensors(model.state_dict())
135 counter_before_loading_weights = time.perf_counter()
136 for key, tensor in self.iterate_over_files(filepaths):
137 # If loading with LoRA enabled, additional padding may
138 # be added to certain parameters. We only load into a
139 # narrowed view of the parameter data.
140 param_data = state_dict[key].data
141 param_shape = state_dict[key].shape
142 for dim, size in enumerate(tensor.shape):
143 if size < param_shape[dim]:
144 param_data = param_data.narrow(dim, 0, size)
145 if tensor.shape != param_shape:
146 logger.warning(
147 "loading tensor of shape %s into parameter '%s' of shape %s",
148 tensor.shape,
149 key,
150 param_shape,
151 )
152 param_data.copy_(tensor)
153 state_dict.pop(key)
154 counter_after_loading_weights = time.perf_counter()
155 logger.info_once(
156 "Loading weights took %.2f seconds",
157 counter_after_loading_weights - counter_before_loading_weights,
158 )
159 if state_dict:
160 raise ValueError(f"Missing keys {tuple(state_dict)} in loaded state!")
161
162 def iterate_over_files(
163 self, paths
164 ) -> Generator[tuple[str, torch.Tensor], None, None]:
165 if self.load_config.load_format == "runai_streamer_sharded":
166 yield from runai_safetensors_weights_iterator(paths, True)
167 else:
168 from safetensors.torch import safe_open
169
170 for path in paths:
171 with safe_open(path, framework="pt") as f:
172 for key in f.keys(): # noqa: SIM118
173 tensor = f.get_tensor(key)
174 yield key, tensor
175
176 @staticmethod
177 def save_model(
178 model: torch.nn.Module,
179 path: str,
180 pattern: str | None = None,
181 max_size: int | None = None,
182 ) -> None:
183 from safetensors.torch import save_file
184
185 from vllm.distributed import get_tensor_model_parallel_rank
186
187 if pattern is None:
188 pattern = ShardedStateLoader.DEFAULT_PATTERN
189 rank = get_tensor_model_parallel_rank()
190 part_idx = 0
191 total_size = 0
192 state_dict = ShardedStateLoader._filter_subtensors(model.state_dict())
193 state_dict_part: dict[str, torch.Tensor] = {}
194 for key, tensor in state_dict.items():
195 param_size = tensor.nelement() * tensor.element_size()
196 if max_size is not None and total_size + param_size > max_size:
197 filename = pattern.format(rank=rank, part=part_idx)
198 save_file(
199 state_dict_part,
200 os.path.join(path, filename),
201 )
202 part_idx += 1
203 total_size = 0
204 state_dict_part = {}
205 state_dict_part[key] = tensor
206 total_size += param_size
207 if len(state_dict_part) > 0:
208 filename = pattern.format(rank=rank, part=part_idx)
209 save_file(
210 state_dict_part,
211 os.path.join(path, filename),
212 )