Skip to content

Commit 4d6d2f5

Browse files
committed
refactor(convert): move shared checkpoint utils to convert.utils
Move download_checkpoint_from_hub, ensure_checkpoint_is_local, load_checkpoint_config, and load_checkpoint_weights from convert/eagle/utils.py to convert/utils.py so they can be shared across converter implementations without cross-algorithm imports. Update all consumers (eagle, eagle3, mtp converters and their tests) to import from the canonical location. Signed-off-by: Rahul-Tuli <rtuli@redhat.com>
1 parent 21ea130 commit 4d6d2f5

7 files changed

Lines changed: 162 additions & 154 deletions

File tree

src/speculators/convert/eagle/eagle3_converter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
from speculators.convert.eagle.utils import (
1414
build_llama_config_dtype_kwarg,
1515
build_llama_config_rope_kwargs,
16-
ensure_checkpoint_is_local,
1716
find_vocab_size,
17+
)
18+
from speculators.convert.utils import (
19+
ensure_checkpoint_is_local,
1820
load_checkpoint_config,
1921
load_checkpoint_weights,
2022
)

src/speculators/convert/eagle/eagle_converter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from speculators.convert.eagle.utils import (
1717
build_llama_config_rope_kwargs,
1818
detect_fusion_bias_and_layernorms,
19+
)
20+
from speculators.convert.utils import (
1921
ensure_checkpoint_is_local,
2022
load_checkpoint_config,
2123
load_checkpoint_weights,

src/speculators/convert/eagle/utils.py

Lines changed: 1 addition & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
"""
2-
Utility functions for checkpoint conversion operations.
2+
Utility functions for Eagle checkpoint conversion operations.
33
"""
44

55
from __future__ import annotations
66

77
import inspect
8-
import json
9-
from pathlib import Path
108
from typing import Any
119

1210
import torch
13-
from huggingface_hub import snapshot_download
1411
from loguru import logger
15-
from safetensors import safe_open
1612
from transformers import LlamaConfig
1713

1814
_LLAMA_CONFIG_PARAMS = set(inspect.signature(LlamaConfig.__init__).parameters)
@@ -69,139 +65,6 @@ def find_vocab_size(config_dict: dict) -> int | None:
6965
return None
7066

7167

72-
def download_checkpoint_from_hub(model_id: str, cache_dir: str | None = None) -> Path:
73-
"""
74-
Download a checkpoint from HuggingFace Hub.
75-
76-
:param model_id: HuggingFace model ID
77-
:param cache_dir: Optional directory to cache downloads
78-
:return: Local path to the downloaded checkpoint
79-
:raises FileNotFoundError: If the checkpoint cannot be downloaded
80-
81-
:Example:
82-
83-
>>> path = download_checkpoint_from_hub("yuhuili/EAGLE-LLaMA3.1-Instruct-8B")
84-
>>> print(path)
85-
/home/user/.cache/huggingface/hub/models--yuhuili--EAGLE-LLaMA3.1-Instruct-8B/snapshots/...
86-
"""
87-
logger.info(f"Downloading checkpoint from HuggingFace: {model_id}")
88-
try:
89-
local_path = snapshot_download(
90-
repo_id=model_id,
91-
allow_patterns=["*.json", "*.safetensors", "*.bin", "*.index.json"],
92-
cache_dir=cache_dir,
93-
)
94-
logger.debug(f"Downloaded to: {local_path}")
95-
return Path(local_path)
96-
except Exception as hf_exception:
97-
logger.error(f"Failed to download checkpoint: {hf_exception}")
98-
raise FileNotFoundError(f"Checkpoint not found: {model_id}") from hf_exception
99-
100-
101-
def ensure_checkpoint_is_local(
102-
checkpoint_path: str | Path, cache_dir: str | Path | None = None
103-
) -> Path:
104-
"""
105-
Ensure we have a local copy of the checkpoint.
106-
107-
If the path exists locally, return it. Otherwise, treat it as a
108-
HuggingFace model ID and download it.
109-
110-
:param checkpoint_path: Local path or HuggingFace model ID
111-
:param cache_dir: Optional cache directory for downloads
112-
:return: Path to local checkpoint directory
113-
114-
:Example:
115-
116-
>>> # Local path - returned as-is
117-
>>> local = ensure_checkpoint_is_local("./my_checkpoint")
118-
119-
>>> # HuggingFace ID - downloaded first
120-
>>> downloaded = ensure_checkpoint_is_local(
121-
... "yuhuili/EAGLE-LLaMA3.1-Instruct-8B"
122-
... )
123-
"""
124-
checkpoint_path = Path(checkpoint_path)
125-
126-
if checkpoint_path.exists():
127-
logger.debug(f"Using local checkpoint: {checkpoint_path}")
128-
return checkpoint_path
129-
130-
return download_checkpoint_from_hub(
131-
model_id=str(checkpoint_path), cache_dir=str(cache_dir) if cache_dir else None
132-
)
133-
134-
135-
def load_checkpoint_config(checkpoint_dir: Path) -> dict:
136-
"""
137-
Load the config.json from a checkpoint directory.
138-
139-
:param checkpoint_dir: Path to checkpoint directory
140-
:return: Config dictionary
141-
:raises FileNotFoundError: If config.json is not found
142-
143-
:Example:
144-
145-
>>> config = load_checkpoint_config(Path("./checkpoint"))
146-
>>> print(config["model_type"])
147-
llama
148-
"""
149-
config_path = checkpoint_dir / "config.json"
150-
if not config_path.exists():
151-
raise FileNotFoundError(f"No config.json found at {checkpoint_dir}")
152-
153-
logger.debug(f"Loading config from: {config_path}")
154-
with config_path.open() as f:
155-
return json.load(f)
156-
157-
158-
def load_checkpoint_weights(checkpoint_dir: Path) -> dict[str, torch.Tensor]:
159-
"""
160-
Load model weights from a checkpoint directory.
161-
162-
Supports both safetensors and PyTorch bin formats.
163-
164-
:param checkpoint_dir: Path to checkpoint directory
165-
:return: Dictionary mapping weight names to tensors
166-
:raises FileNotFoundError: If no weights are found
167-
:raises NotImplementedError: If checkpoint is sharded
168-
169-
:Example:
170-
171-
>>> weights = load_checkpoint_weights(Path("./checkpoint"))
172-
>>> print(f"Loaded {len(weights)} weights")
173-
Loaded 50 weights
174-
"""
175-
weights = {}
176-
177-
safetensors_path = checkpoint_dir / "model.safetensors"
178-
if safetensors_path.exists():
179-
logger.debug(f"Loading safetensors weights from: {safetensors_path}")
180-
with safe_open(safetensors_path, framework="pt") as f:
181-
# safetensors requires iterating over keys() method
182-
for key in f.keys(): # noqa: SIM118
183-
weights[key] = f.get_tensor(key)
184-
return weights
185-
186-
pytorch_path = checkpoint_dir / "pytorch_model.bin"
187-
if pytorch_path.exists():
188-
logger.debug(f"Loading PyTorch weights from: {pytorch_path}")
189-
return torch.load(pytorch_path, map_location="cpu")
190-
191-
index_paths = [
192-
checkpoint_dir / "model.safetensors.index.json",
193-
checkpoint_dir / "pytorch_model.bin.index.json",
194-
]
195-
for index_path in index_paths:
196-
if index_path.exists():
197-
raise NotImplementedError(
198-
f"Sharded checkpoint detected: {index_path}. "
199-
"Please use a single-file checkpoint."
200-
)
201-
202-
raise FileNotFoundError(f"No weights found at {checkpoint_dir}")
203-
204-
20568
def detect_fusion_bias_and_layernorms(
20669
weights: dict[str, torch.Tensor],
20770
) -> tuple[bool, bool]:

src/speculators/convert/mtp/converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from transformers import PretrainedConfig
2121

2222
from speculators.config import SpeculatorsConfig, VerifierConfig
23-
from speculators.convert.eagle.utils import (
23+
from speculators.convert.utils import (
2424
ensure_checkpoint_is_local,
2525
load_checkpoint_config,
2626
)

src/speculators/convert/utils.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Shared utility functions for checkpoint conversion operations."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
from pathlib import Path
7+
8+
import torch
9+
from huggingface_hub import snapshot_download
10+
from loguru import logger
11+
from safetensors import safe_open
12+
13+
14+
def download_checkpoint_from_hub(model_id: str, cache_dir: str | None = None) -> Path:
15+
"""
16+
Download a checkpoint from HuggingFace Hub.
17+
18+
:param model_id: HuggingFace model ID
19+
:param cache_dir: Optional directory to cache downloads
20+
:return: Local path to the downloaded checkpoint
21+
:raises FileNotFoundError: If the checkpoint cannot be downloaded
22+
23+
:Example:
24+
25+
>>> path = download_checkpoint_from_hub("yuhuili/EAGLE-LLaMA3.1-Instruct-8B")
26+
>>> print(path)
27+
/home/user/.cache/huggingface/hub/models--yuhuili--EAGLE-LLaMA3.1-Instruct-8B/snapshots/...
28+
"""
29+
logger.info(f"Downloading checkpoint from HuggingFace: {model_id}")
30+
try:
31+
local_path = snapshot_download(
32+
repo_id=model_id,
33+
allow_patterns=["*.json", "*.safetensors", "*.bin", "*.index.json"],
34+
cache_dir=cache_dir,
35+
)
36+
logger.debug(f"Downloaded to: {local_path}")
37+
return Path(local_path)
38+
except Exception as hf_exception:
39+
logger.error(f"Failed to download checkpoint: {hf_exception}")
40+
raise FileNotFoundError(f"Checkpoint not found: {model_id}") from hf_exception
41+
42+
43+
def ensure_checkpoint_is_local(
44+
checkpoint_path: str | Path, cache_dir: str | Path | None = None
45+
) -> Path:
46+
"""
47+
Ensure we have a local copy of the checkpoint.
48+
49+
If the path exists locally, return it. Otherwise, treat it as a
50+
HuggingFace model ID and download it.
51+
52+
:param checkpoint_path: Local path or HuggingFace model ID
53+
:param cache_dir: Optional cache directory for downloads
54+
:return: Path to local checkpoint directory
55+
56+
:Example:
57+
58+
>>> # Local path - returned as-is
59+
>>> local = ensure_checkpoint_is_local("./my_checkpoint")
60+
61+
>>> # HuggingFace ID - downloaded first
62+
>>> downloaded = ensure_checkpoint_is_local(
63+
... "yuhuili/EAGLE-LLaMA3.1-Instruct-8B"
64+
... )
65+
"""
66+
checkpoint_path = Path(checkpoint_path)
67+
68+
if checkpoint_path.exists():
69+
logger.debug(f"Using local checkpoint: {checkpoint_path}")
70+
return checkpoint_path
71+
72+
return download_checkpoint_from_hub(
73+
model_id=str(checkpoint_path), cache_dir=str(cache_dir) if cache_dir else None
74+
)
75+
76+
77+
def load_checkpoint_config(checkpoint_dir: Path) -> dict:
78+
"""
79+
Load the config.json from a checkpoint directory.
80+
81+
:param checkpoint_dir: Path to checkpoint directory
82+
:return: Config dictionary
83+
:raises FileNotFoundError: If config.json is not found
84+
85+
:Example:
86+
87+
>>> config = load_checkpoint_config(Path("./checkpoint"))
88+
>>> print(config["model_type"])
89+
llama
90+
"""
91+
config_path = checkpoint_dir / "config.json"
92+
if not config_path.exists():
93+
raise FileNotFoundError(f"No config.json found at {checkpoint_dir}")
94+
95+
logger.debug(f"Loading config from: {config_path}")
96+
with config_path.open() as f:
97+
return json.load(f)
98+
99+
100+
def load_checkpoint_weights(checkpoint_dir: Path) -> dict[str, torch.Tensor]:
101+
"""
102+
Load model weights from a checkpoint directory.
103+
104+
Supports both safetensors and PyTorch bin formats.
105+
106+
:param checkpoint_dir: Path to checkpoint directory
107+
:return: Dictionary mapping weight names to tensors
108+
:raises FileNotFoundError: If no weights are found
109+
:raises NotImplementedError: If checkpoint is sharded
110+
111+
:Example:
112+
113+
>>> weights = load_checkpoint_weights(Path("./checkpoint"))
114+
>>> print(f"Loaded {len(weights)} weights")
115+
Loaded 50 weights
116+
"""
117+
weights = {}
118+
119+
safetensors_path = checkpoint_dir / "model.safetensors"
120+
if safetensors_path.exists():
121+
logger.debug(f"Loading safetensors weights from: {safetensors_path}")
122+
with safe_open(safetensors_path, framework="pt") as f:
123+
for key in f.keys(): # noqa: SIM118
124+
weights[key] = f.get_tensor(key)
125+
return weights
126+
127+
pytorch_path = checkpoint_dir / "pytorch_model.bin"
128+
if pytorch_path.exists():
129+
logger.debug(f"Loading PyTorch weights from: {pytorch_path}")
130+
return torch.load(pytorch_path, map_location="cpu")
131+
132+
index_paths = [
133+
checkpoint_dir / "model.safetensors.index.json",
134+
checkpoint_dir / "pytorch_model.bin.index.json",
135+
]
136+
for index_path in index_paths:
137+
if index_path.exists():
138+
raise NotImplementedError(
139+
f"Sharded checkpoint detected: {index_path}. "
140+
"Please use a single-file checkpoint."
141+
)
142+
143+
raise FileNotFoundError(f"No weights found at {checkpoint_dir}")

tests/unit/convert/test_eagle3_converter.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
import torch
1515

1616
from speculators.convert.eagle.eagle3_converter import Eagle3Converter
17-
from speculators.convert.eagle.utils import (
18-
load_checkpoint_config,
19-
)
17+
from speculators.convert.utils import load_checkpoint_config
2018

2119

2220
class TestEagle3ConverterFixes:

0 commit comments

Comments
 (0)