Skip to content

Commit 8a3757b

Browse files
authored
Merge pull request #1756 from engelmi/fix-big-model-fetching
Fix assembling URLs for big models
2 parents 5098076 + acc63af commit 8a3757b

File tree

2 files changed

+100
-32
lines changed

2 files changed

+100
-32
lines changed

ramalama/url.py

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,35 @@ def extract_model_identifiers(self):
7575

7676
return model_name, model_tag, model_organization
7777

78+
def _assemble_split_file_list(self, snapshot_hash: str) -> list[SnapshotFile]:
79+
files: list[SnapshotFile] = []
80+
81+
# model is split, lets fetch all files based on the name pattern
82+
match = re.match(SPLIT_MODEL_PATH_RE, self.model)
83+
if match is None:
84+
return files
85+
86+
path_part = match[1]
87+
filename_base = match[2]
88+
total_parts = int(match[3])
89+
90+
for i in range(1, total_parts + 1):
91+
file_name = f"{filename_base}-{i:05d}-of-{total_parts:05d}.gguf"
92+
url = f"{self.type}://{path_part}/{file_name}"
93+
files.append(
94+
SnapshotFile(
95+
url=url,
96+
header={},
97+
hash=snapshot_hash,
98+
type=SnapshotFileType.Model,
99+
name=file_name,
100+
should_show_progress=True,
101+
required=True,
102+
)
103+
)
104+
105+
return files
106+
78107
def pull(self, _):
79108
name, tag, _ = self.extract_model_identifiers()
80109
_, _, all_files = self.model_store.get_cached_files(tag)
@@ -96,40 +125,21 @@ def pull(self, _):
96125
self.model_store.new_snapshot(tag, snapshot_hash, files)
97126
return
98127

99-
if not is_split_file_model(self.model):
100-
files.append(
101-
SnapshotFile(
102-
url=f"{self.type}://{self.model}",
103-
header={},
104-
hash=snapshot_hash,
105-
type=SnapshotFileType.Model,
106-
name=name,
107-
should_show_progress=True,
108-
required=True,
109-
)
110-
)
128+
if is_split_file_model(self.model):
129+
files = self._assemble_split_file_list(snapshot_hash)
111130
self.model_store.new_snapshot(tag, snapshot_hash, files)
112131
return
113132

114-
# model is split, lets fetch all files based on the name pattern
115-
match = re.match(SPLIT_MODEL_PATH_RE, self.model)
116-
path_part = match[1]
117-
filename_base = match[2]
118-
total_parts = int(match[3])
119-
120-
for i in range(total_parts - 1):
121-
i_off = i + 2
122-
url = f"{self.type}://{path_part}/{filename_base}-{i_off:05d}-of-{total_parts:05d}.gguf"
123-
files.append(
124-
SnapshotFile(
125-
url=url,
126-
header={},
127-
hash=snapshot_hash,
128-
type=SnapshotFileType.Model,
129-
name=name,
130-
should_show_progress=True,
131-
required=True,
132-
)
133+
files.append(
134+
SnapshotFile(
135+
url=f"{self.type}://{self.model}",
136+
header={},
137+
hash=snapshot_hash,
138+
type=SnapshotFileType.Model,
139+
name=name,
140+
should_show_progress=True,
141+
required=True,
133142
)
134-
143+
)
135144
self.model_store.new_snapshot(tag, snapshot_hash, files)
145+
return

test/unit/test_url.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from dataclasses import dataclass, field
2+
3+
import pytest
4+
5+
from ramalama.model_store.snapshot_file import SnapshotFile
6+
from ramalama.url import URL
7+
8+
9+
@dataclass
10+
class Input:
11+
Model: str
12+
13+
14+
@dataclass
15+
class Expected:
16+
URLList: list[str] = field(default_factory=lambda: [])
17+
Names: list[str] = field(default_factory=lambda: [])
18+
19+
20+
@pytest.mark.parametrize(
21+
"input,expected",
22+
[
23+
(Input(""), Expected()),
24+
(Input("file:///tmp/models/granite-3b-code-base.Q4_K_M.gguf"), Expected()),
25+
(
26+
Input(
27+
"huggingface.co/unsloth/Qwen3-Coder-480B-A35B-Instruct-GGUF/resolve/main/Q3_K_M/Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00001-of-00005.gguf" # noqa: E501
28+
),
29+
Expected(
30+
URLList=[
31+
"https://huggingface.co/unsloth/Qwen3-Coder-480B-A35B-Instruct-GGUF/resolve/main/Q3_K_M/Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00001-of-00005.gguf", # noqa: E501
32+
"https://huggingface.co/unsloth/Qwen3-Coder-480B-A35B-Instruct-GGUF/resolve/main/Q3_K_M/Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00002-of-00005.gguf", # noqa: E501
33+
"https://huggingface.co/unsloth/Qwen3-Coder-480B-A35B-Instruct-GGUF/resolve/main/Q3_K_M/Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00003-of-00005.gguf", # noqa: E501
34+
"https://huggingface.co/unsloth/Qwen3-Coder-480B-A35B-Instruct-GGUF/resolve/main/Q3_K_M/Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00004-of-00005.gguf", # noqa: E501
35+
"https://huggingface.co/unsloth/Qwen3-Coder-480B-A35B-Instruct-GGUF/resolve/main/Q3_K_M/Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00005-of-00005.gguf", # noqa: E501
36+
],
37+
Names=[
38+
"Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00001-of-00005.gguf",
39+
"Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00002-of-00005.gguf",
40+
"Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00003-of-00005.gguf",
41+
"Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00004-of-00005.gguf",
42+
"Qwen3-Coder-480B-A35B-Instruct-Q3_K_M-00005-of-00005.gguf",
43+
],
44+
),
45+
),
46+
],
47+
)
48+
def test__assemble_split_file_list(input: Input, expected: Expected):
49+
# store path and scheme irrelevant here
50+
model = URL(input.Model, "/store", "https")
51+
files: list[SnapshotFile] = model._assemble_split_file_list("doesnotmatterhere")
52+
file_count = len(files)
53+
assert file_count == len(expected.Names)
54+
assert file_count == len(expected.URLList)
55+
56+
for i in range(file_count):
57+
assert files[i].url == expected.URLList[i]
58+
assert files[i].name == expected.Names[i]

0 commit comments

Comments
 (0)