Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +108 -47
  3. requirements.txt +1 -1
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: πŸ”€
4
  colorFrom: yellow
5
  colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 4.24.0
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
 
4
  colorFrom: yellow
5
  colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 4.44.1
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
app.py CHANGED
@@ -3,10 +3,9 @@ import pathlib
3
  import random
4
  import string
5
  import tempfile
6
- import time
7
- from concurrent.futures import ThreadPoolExecutor
8
  from typing import Iterable, List
9
 
 
10
  import gradio as gr
11
  import huggingface_hub
12
  import torch
@@ -16,44 +15,14 @@ from mergekit.config import MergeConfiguration
16
 
17
  from clean_community_org import garbage_collect_empty_models
18
  from apscheduler.schedulers.background import BackgroundScheduler
19
- from datetime import datetime, timezone
20
-
21
- has_gpu = torch.cuda.is_available()
22
-
23
- # Running directly from Python doesn't work well with Gradio+run_process because of:
24
- # Cannot re-initialize CUDA in forked subprocess. To use CUDA with multiprocessing, you must use the 'spawn' start method
25
- # Let's use the CLI instead.
26
- #
27
- # import mergekit.merge
28
- # from mergekit.common import parse_kmb
29
- # from mergekit.options import MergeOptions
30
- #
31
- # merge_options = (
32
- # MergeOptions(
33
- # copy_tokenizer=True,
34
- # cuda=True,
35
- # low_cpu_memory=True,
36
- # write_model_card=True,
37
- # )
38
- # if has_gpu
39
- # else MergeOptions(
40
- # allow_crimes=True,
41
- # out_shard_size=parse_kmb("1B"),
42
- # lazy_unpickle=True,
43
- # write_model_card=True,
44
- # )
45
- # )
46
-
47
- cli = "mergekit-yaml config.yaml merge --copy-tokenizer" + (
48
- " --cuda --low-cpu-memory --allow-crimes" if has_gpu else " --allow-crimes --out-shard-size 1B --lazy-unpickle"
49
- )
50
 
51
  MARKDOWN_DESCRIPTION = """
52
  # mergekit-gui
53
 
54
  The fastest way to perform a model merge πŸ”₯
55
 
56
- Specify a YAML configuration file (see examples below) and a HF token and this app will perform the merge and upload the merged model to your user profile.
57
  """
58
 
59
  MARKDOWN_ARTICLE = """
@@ -113,11 +82,56 @@ examples = [[str(f)] for f in pathlib.Path("examples").glob("*.yaml")]
113
  COMMUNITY_HF_TOKEN = os.getenv("COMMUNITY_HF_TOKEN")
114
 
115
 
116
- def merge(yaml_config: str, hf_token: str, repo_name: str) -> Iterable[List[Log]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  runner = LogsViewRunner()
118
 
119
  if not yaml_config:
120
- yield runner.log("Empty yaml, pick an example below", level="ERROR")
121
  return
122
  try:
123
  merge_config = MergeConfiguration.model_validate(yaml.safe_load(yaml_config))
@@ -127,6 +141,12 @@ def merge(yaml_config: str, hf_token: str, repo_name: str) -> Iterable[List[Log]
127
 
128
  is_community_model = False
129
  if not hf_token:
 
 
 
 
 
 
130
  if "/" in repo_name and not repo_name.startswith("mergekit-community/"):
131
  yield runner.log(
132
  f"Cannot upload merge model to namespace {repo_name.split('/')[0]}: you must provide a valid token.",
@@ -142,6 +162,10 @@ def merge(yaml_config: str, hf_token: str, repo_name: str) -> Iterable[List[Log]
142
  hf_token = COMMUNITY_HF_TOKEN
143
 
144
  api = huggingface_hub.HfApi(token=hf_token)
 
 
 
 
145
 
146
  with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdirname:
147
  tmpdir = pathlib.Path(tmpdirname)
@@ -163,19 +187,42 @@ def merge(yaml_config: str, hf_token: str, repo_name: str) -> Iterable[List[Log]
163
 
164
  try:
165
  yield runner.log(f"Creating repo {repo_name}")
166
- repo_url = api.create_repo(repo_name, exist_ok=True)
167
  yield runner.log(f"Repo created: {repo_url}")
168
  except Exception as e:
169
  yield runner.log(f"Error creating repo {e}", level="ERROR")
170
  return
171
 
172
- # Set tmp HF_HOME to avoid filling up disk Space
173
- tmp_env = os.environ.copy() # taken from https://stackoverflow.com/a/4453495
174
- tmp_env["HF_HOME"] = f"{tmpdirname}/.cache"
175
- full_cli = cli + f" --lora-merge-cache {tmpdirname}/.lora_cache"
176
- yield from runner.run_command(full_cli.split(), cwd=merged_path, env=tmp_env)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- if runner.exit_code != 0:
 
179
  yield runner.log("Merge failed. Deleting repo as no model is uploaded.", level="ERROR")
180
  api.delete_repo(repo_url.repo_id)
181
  return
@@ -188,9 +235,18 @@ def merge(yaml_config: str, hf_token: str, repo_name: str) -> Iterable[List[Log]
188
  )
189
  yield runner.log(f"Model successfully uploaded to HF: {repo_url.repo_id}")
190
 
 
 
 
 
 
191
  # This is workaround. As the space always getting stuck.
192
  def _restart_space():
193
- huggingface_hub.HfApi().restart_space(repo_id="arcee-ai/mergekit-gui", token=COMMUNITY_HF_TOKEN, factory_reboot=False)
 
 
 
 
194
  # Run garbage collection every hour to keep the community org clean.
195
  # Empty models might exists if the merge fails abruptly (e.g. if user leaves the Space).
196
  def _garbage_remover():
@@ -199,6 +255,7 @@ def _garbage_remover():
199
  except Exception as e:
200
  print("Error running garbage collection", e)
201
 
 
202
  scheduler = BackgroundScheduler()
203
  restart_space_job = scheduler.add_job(_restart_space, "interval", seconds=21600)
204
  garbage_remover_job = scheduler.add_job(_garbage_remover, "interval", seconds=3600)
@@ -210,7 +267,7 @@ NEXT_RESTART = f"Next Restart: {next_run_time_utc.strftime('%Y-%m-%d %H:%M:%S')}
210
  with gr.Blocks() as demo:
211
  gr.Markdown(MARKDOWN_DESCRIPTION)
212
  gr.Markdown(NEXT_RESTART)
213
-
214
  with gr.Row():
215
  filename = gr.Textbox(visible=False, label="filename")
216
  config = gr.Code(language="yaml", lines=10, label="config.yaml")
@@ -227,6 +284,11 @@ with gr.Blocks() as demo:
227
  label="Repo name",
228
  placeholder="Optional. Will create a random name if empty.",
229
  )
 
 
 
 
 
230
  button = gr.Button("Merge", variant="primary")
231
  logs = LogsView(label="Terminal output")
232
  gr.Examples(
@@ -239,8 +301,7 @@ with gr.Blocks() as demo:
239
  )
240
  gr.Markdown(MARKDOWN_ARTICLE)
241
 
242
- button.click(fn=merge, inputs=[config, token, repo_name], outputs=[logs])
243
-
244
 
245
 
246
  demo.queue(default_concurrency_limit=1).launch()
 
3
  import random
4
  import string
5
  import tempfile
 
 
6
  from typing import Iterable, List
7
 
8
+ import spaces
9
  import gradio as gr
10
  import huggingface_hub
11
  import torch
 
15
 
16
  from clean_community_org import garbage_collect_empty_models
17
  from apscheduler.schedulers.background import BackgroundScheduler
18
+ from datetime import timezone
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  MARKDOWN_DESCRIPTION = """
21
  # mergekit-gui
22
 
23
  The fastest way to perform a model merge πŸ”₯
24
 
25
+ Specify a YAML configuration file (see examples below) and a HF token and this app will perform the merge and upload the merged model to your user profile. Uses Zero GPU quota to perform the merge.
26
  """
27
 
28
  MARKDOWN_ARTICLE = """
 
82
  COMMUNITY_HF_TOKEN = os.getenv("COMMUNITY_HF_TOKEN")
83
 
84
 
85
+ def run_merge_cpu(runner: LogsViewRunner, cli: str, merged_path: str, tmpdirname: str):
86
+ # Set tmp HF_HOME to avoid filling up disk Space
87
+ tmp_env = os.environ.copy()
88
+ tmp_env["HF_HOME"] = f"{tmpdirname}/.cache"
89
+ full_cli = cli + f" --lora-merge-cache {tmpdirname}/.lora_cache --transformers-cache {tmpdirname}/.cache"
90
+ yield from runner.run_command(full_cli.split(), cwd=merged_path, env=tmp_env)
91
+ yield ("done", runner.exit_code)
92
+
93
+
94
+ @spaces.GPU(duration=60 * 5)
95
+ def run_merge_gpu(runner: LogsViewRunner, cli: str, merged_path: str, tmpdirname: str):
96
+ yield from run_merge_cpu(
97
+ runner,
98
+ cli,
99
+ merged_path,
100
+ tmpdirname,
101
+ )
102
+
103
+
104
+ def run_merge(
105
+ runner: LogsViewRunner,
106
+ cli: str,
107
+ merged_path: str,
108
+ tmpdirname: str,
109
+ use_gpu: bool,
110
+ ):
111
+ if use_gpu:
112
+ yield from run_merge_gpu(runner, cli, merged_path, tmpdirname)
113
+ else:
114
+ yield from run_merge_cpu(runner, cli, merged_path, tmpdirname)
115
+
116
+
117
+ def prefetch_models(
118
+ runner: LogsViewRunner,
119
+ merge_config: MergeConfiguration,
120
+ hf_home: str,
121
+ lora_merge_cache: str,
122
+ ):
123
+ for model in merge_config.referenced_models():
124
+ yield runner.log(f"Downloading {model}...")
125
+ model = model.merged(cache_dir=lora_merge_cache, trust_remote_code=False)
126
+ local_path = model.local_path(cache_dir=hf_home)
127
+ yield runner.log(f"\tDownloaded {model} to {local_path}")
128
+
129
+
130
+ def merge(yaml_config: str, hf_token: str, repo_name: str, private: bool) -> Iterable[List[Log]]:
131
  runner = LogsViewRunner()
132
 
133
  if not yaml_config:
134
+ yield runner.log("Empty yaml, enter your config or pick an example below", level="ERROR")
135
  return
136
  try:
137
  merge_config = MergeConfiguration.model_validate(yaml.safe_load(yaml_config))
 
141
 
142
  is_community_model = False
143
  if not hf_token:
144
+ if private:
145
+ yield runner.log(
146
+ "Cannot upload model as private without a token. Please provide a HF token.",
147
+ level="ERROR",
148
+ )
149
+ return
150
  if "/" in repo_name and not repo_name.startswith("mergekit-community/"):
151
  yield runner.log(
152
  f"Cannot upload merge model to namespace {repo_name.split('/')[0]}: you must provide a valid token.",
 
162
  hf_token = COMMUNITY_HF_TOKEN
163
 
164
  api = huggingface_hub.HfApi(token=hf_token)
165
+ has_gpu = torch.cuda.is_available()
166
+ cli = "mergekit-yaml config.yaml merge --copy-tokenizer --allow-crimes -v" + (
167
+ " --cuda --low-cpu-memory --read-to-gpu" if (has_gpu) else " --out-shard-size 1B --lazy-unpickle"
168
+ )
169
 
170
  with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdirname:
171
  tmpdir = pathlib.Path(tmpdirname)
 
187
 
188
  try:
189
  yield runner.log(f"Creating repo {repo_name}")
190
+ repo_url = api.create_repo(repo_name, exist_ok=True, private=private, repo_type="model")
191
  yield runner.log(f"Repo created: {repo_url}")
192
  except Exception as e:
193
  yield runner.log(f"Error creating repo {e}", level="ERROR")
194
  return
195
 
196
+ # Prefetch models to avoid downloading them with scarce GPU time
197
+ yield runner.log("Prefetching models...")
198
+ yield from prefetch_models(
199
+ runner,
200
+ merge_config,
201
+ hf_home=f"{tmpdirname}/.cache",
202
+ lora_merge_cache=f"{tmpdirname}/.lora_cache",
203
+ )
204
+ yield runner.log("Models prefetched. Starting merge.")
205
+ exit_code = None
206
+ try:
207
+ for ev in run_merge(
208
+ runner,
209
+ cli,
210
+ merged_path,
211
+ tmpdirname,
212
+ use_gpu=has_gpu,
213
+ ):
214
+ if isinstance(ev, tuple) and ev[0] == "done":
215
+ exit_code = ev[1]
216
+ continue
217
+ yield ev
218
+ except Exception as e:
219
+ yield runner.log(f"Error running merge {e}", level="ERROR")
220
+ yield runner.log("Merge failed. Deleting repo as no model is uploaded.", level="ERROR")
221
+ api.delete_repo(repo_url.repo_id)
222
+ return
223
 
224
+ if exit_code != 0:
225
+ yield runner.log(f"Exit code: {exit_code}")
226
  yield runner.log("Merge failed. Deleting repo as no model is uploaded.", level="ERROR")
227
  api.delete_repo(repo_url.repo_id)
228
  return
 
235
  )
236
  yield runner.log(f"Model successfully uploaded to HF: {repo_url.repo_id}")
237
 
238
+
239
+ merge.zerogpu = True
240
+ run_merge.zerogpu = True
241
+
242
+
243
  # This is workaround. As the space always getting stuck.
244
  def _restart_space():
245
+ huggingface_hub.HfApi().restart_space(
246
+ repo_id="arcee-ai/mergekit-gui", token=COMMUNITY_HF_TOKEN, factory_reboot=False
247
+ )
248
+
249
+
250
  # Run garbage collection every hour to keep the community org clean.
251
  # Empty models might exists if the merge fails abruptly (e.g. if user leaves the Space).
252
  def _garbage_remover():
 
255
  except Exception as e:
256
  print("Error running garbage collection", e)
257
 
258
+
259
  scheduler = BackgroundScheduler()
260
  restart_space_job = scheduler.add_job(_restart_space, "interval", seconds=21600)
261
  garbage_remover_job = scheduler.add_job(_garbage_remover, "interval", seconds=3600)
 
267
  with gr.Blocks() as demo:
268
  gr.Markdown(MARKDOWN_DESCRIPTION)
269
  gr.Markdown(NEXT_RESTART)
270
+
271
  with gr.Row():
272
  filename = gr.Textbox(visible=False, label="filename")
273
  config = gr.Code(language="yaml", lines=10, label="config.yaml")
 
284
  label="Repo name",
285
  placeholder="Optional. Will create a random name if empty.",
286
  )
287
+ private = gr.Checkbox(
288
+ label="Private",
289
+ value=False,
290
+ info="Upload the model as private. If not checked, will be public. Must provide a token.",
291
+ )
292
  button = gr.Button("Merge", variant="primary")
293
  logs = LogsView(label="Terminal output")
294
  gr.Examples(
 
301
  )
302
  gr.Markdown(MARKDOWN_ARTICLE)
303
 
304
+ button.click(fn=merge, inputs=[config, token, repo_name, private], outputs=[logs])
 
305
 
306
 
307
  demo.queue(default_concurrency_limit=1).launch()
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  apscheduler
2
- torch
3
  git+https://github.com/arcee-ai/mergekit.git
4
  # see https://huggingface.co/spaces/Wauplin/gradio_logsview
5
  gradio_logsview@https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.5-py3-none-any.whl
 
1
  apscheduler
2
+ torch==2.5.1
3
  git+https://github.com/arcee-ai/mergekit.git
4
  # see https://huggingface.co/spaces/Wauplin/gradio_logsview
5
  gradio_logsview@https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.5-py3-none-any.whl