-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[WIP][None][feat] add layer-wise safetensors loading for DeepSeek V4 #16437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3e74fe2
2217f86
ab95fcb
a0412a4
e1898ae
7c9c979
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,12 +13,15 @@ | |
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| import glob | ||
| import json | ||
| import multiprocessing | ||
| import os | ||
| import re | ||
| import threading | ||
| from collections import OrderedDict | ||
| from concurrent.futures import ThreadPoolExecutor | ||
| from typing import Any, List | ||
| from contextlib import ExitStack | ||
| from typing import Any, Iterator, List, Tuple | ||
|
|
||
| import psutil | ||
| import safetensors | ||
|
|
@@ -54,6 +57,116 @@ class HfWeightLoader(BaseWeightLoader): | |
| Loads weights from SafeTensors/bin/pth files. | ||
| """ | ||
|
|
||
| @staticmethod | ||
| def _layer_bucket_name(weight_name: str) -> Tuple[str, int]: | ||
| """Return a stable, layer-atomic bucket for a checkpoint tensor.""" | ||
| match = re.match(r"^(?:model\.)?layers\.(\d+)\.", weight_name) | ||
| if match is not None: | ||
| return ("layer", int(match.group(1))) | ||
| match = re.match(r"^mtp\.(\d+)\.", weight_name) | ||
| if match is not None: | ||
| return ("mtp", int(match.group(1))) | ||
| return ("top", 0) | ||
|
|
||
| def iter_layer_weight_buckets(self, | ||
| checkpoint_dir: str, | ||
| mapping: Mapping, | ||
| use_consolidated: bool = False, | ||
| **kwargs) -> Iterator[ConsumableWeightsDict]: | ||
| """Yield real CPU tensors one semantic layer at a time. | ||
|
|
||
| The safetensors index is used only as metadata. Tensor storage remains | ||
| mmap-backed and the file handles for a bucket stay alive until the | ||
| caller advances the iterator. This keeps cross-shard, same-layer | ||
| fusions atomic without materializing the complete checkpoint. | ||
|
|
||
| Args: | ||
| checkpoint_dir: Directory containing safetensors files and an | ||
| optional Hugging Face safetensors index. | ||
| mapping: Distributed mapping accepted for parity with eager | ||
| checkpoint loading. Bucketing itself is rank-independent. | ||
| use_consolidated: Select consolidated files instead of ordinary | ||
| Hugging Face shards. | ||
| **kwargs: Extra eager-loader arguments that do not affect bucket | ||
| planning. | ||
|
|
||
| Yields: | ||
| A consumable dictionary for all top-level tensors or one numeric | ||
| base/MTP layer. File handles remain open until the next iteration. | ||
|
|
||
| Raises: | ||
| RuntimeError: If the selected safetensors files are missing or | ||
| their index/key metadata is incomplete or ambiguous. | ||
| """ | ||
| del mapping, kwargs | ||
| weight_files = glob.glob(f"{checkpoint_dir}/*.safetensors") | ||
| weight_files = [ | ||
| path for path in weight_files | ||
| if ("consolidated" in os.path.basename(path)) == use_consolidated | ||
| ] | ||
| if not weight_files: | ||
| checkpoint_kind = "consolidated " if use_consolidated else "" | ||
| raise RuntimeError( | ||
| f"Layer-wise loading requires {checkpoint_kind}safetensors " | ||
| f"weights in {checkpoint_dir}.") | ||
|
|
||
| files_by_name = {os.path.basename(path): path for path in weight_files} | ||
| index_files = sorted( | ||
| glob.glob(f"{checkpoint_dir}/*.safetensors.index.json")) | ||
| entries: List[Tuple[str, str]] = [] | ||
| use_index = bool(index_files) and not use_consolidated and all( | ||
| "consolidated" not in name for name in files_by_name) | ||
| if use_index: | ||
| with open(index_files[0], encoding="utf-8") as index_file: | ||
| weight_map = json.load(index_file).get("weight_map", {}) | ||
| missing_files = set(weight_map.values()) - set(files_by_name) | ||
| if missing_files: | ||
| raise RuntimeError( | ||
| f"Safetensors index {index_files[0]} references missing " | ||
| f"checkpoint files: {sorted(missing_files)}") | ||
| for weight_name, file_name in weight_map.items(): | ||
| entries.append((weight_name, files_by_name[file_name])) | ||
|
Comment on lines
+114
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Select and validate the correct safetensors index before bucket loading.
🧰 Tools🪛 ast-grep (0.44.1)[warning] 119-119: File path is request-/variable-derived; validate and normalize to prevent path traversal. (open-filename-from-request) 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Path instructions |
||
| if not entries: | ||
| raise RuntimeError( | ||
| f"Safetensors index {index_files[0]} has an empty weight_map." | ||
| ) | ||
| else: | ||
| seen_keys = set() | ||
| for path in sorted(weight_files): | ||
| with safetensors.safe_open(path, framework="pt", | ||
| device="cpu") as handle: | ||
| for key in handle.keys(): | ||
| if key in seen_keys: | ||
| raise RuntimeError( | ||
| "Duplicate tensor key found across safetensors " | ||
| f"files without an index: {key}") | ||
| seen_keys.add(key) | ||
| entries.append((key, path)) | ||
|
|
||
| buckets = {} | ||
| for weight_name, path in entries: | ||
| bucket = self._layer_bucket_name(weight_name) | ||
| buckets.setdefault(bucket, []).append((weight_name, path)) | ||
|
Comment on lines
+133
to
+149
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: sed -n '1,240p' tensorrt_llm/_torch/models/checkpoints/hf/weight_loader.pyRepository: NVIDIA/TensorRT-LLM Length of output: 10435 🏁 Script executed: sed -n '1,220p' tests/unittest/_torch/models/checkpoints/hf/test_weight_loader.py && python3 - <<'PY'
# Minimal behavioral probe based on the discovered control flow:
# if no tensors are present in any safetensors file and no index is used,
# entries stays empty and the function yields nothing.
entries = []
buckets = {}
for weight_name, path in entries:
bucket = ("top", 0)
buckets.setdefault(bucket, []).append((weight_name, path))
print("entries_empty:", not entries)
print("buckets_empty:", not buckets)
print("would_yield_anything:", bool(sorted(buckets)))
PYRepository: NVIDIA/TensorRT-LLM Length of output: 7706 Reject empty unindexed checkpoints. The no-index path can still return no buckets when every 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| def bucket_sort_key(bucket): | ||
| kind, index = bucket | ||
| return ({"top": 0, "layer": 1, "mtp": 2}[kind], index) | ||
|
|
||
| for bucket in sorted(buckets, key=bucket_sort_key): | ||
| tensors = {} | ||
| with ExitStack() as stack: | ||
| handles = {} | ||
| for weight_name, path in buckets[bucket]: | ||
| if path not in handles: | ||
| handles[path] = stack.enter_context( | ||
| safetensors.safe_open(path, | ||
| framework="pt", | ||
| device="cpu")) | ||
| tensors[weight_name] = handles[path].get_tensor(weight_name) | ||
| logger.info("Loading HF weight bucket %s with %d tensors", | ||
| bucket, len(tensors)) | ||
| yield ConsumableWeightsDict(tensors) | ||
|
|
||
| @staticmethod | ||
| def _is_weight_cache_enabled() -> bool: | ||
| return os.environ.get(_WEIGHT_CACHE_ENV, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Add the required annotations to changed callables.
tensorrt_llm/_torch/models/checkpoints/hf/weight_loader.py#L71-L75: annotate**kwargs.tensorrt_llm/_torch/models/checkpoints/hf/weight_loader.py#L151-L153: annotatebucket_sort_key.tests/unittest/_torch/models/checkpoints/hf/test_weight_loader.py#L21-L256: annotate new tests and nested recording-handle methods.tests/unittest/_torch/modeling/test_modeling_deepseekv4.py#L192-L240: annotate new tests and the parameterizedweights.tensorrt_llm/_torch/pyexecutor/model_loader.py#L1331-L1364: precisely type arguments and-> None.tests/unittest/_torch/executor/test_model_loader_layerwise.py#L18-L208: annotate new helpers, model methods, and tests.As per coding guidelines, “Annotate every function” and prefer precise built-in generic types.
📍 Affects 5 files
tensorrt_llm/_torch/models/checkpoints/hf/weight_loader.py#L71-L75(this comment)tensorrt_llm/_torch/models/checkpoints/hf/weight_loader.py#L151-L153tests/unittest/_torch/models/checkpoints/hf/test_weight_loader.py#L21-L256tests/unittest/_torch/modeling/test_modeling_deepseekv4.py#L192-L240tensorrt_llm/_torch/pyexecutor/model_loader.py#L1331-L1364tests/unittest/_torch/executor/test_model_loader_layerwise.py#L18-L208🤖 Prompt for AI Agents
Source: Coding guidelines