jhsu12 commited on
Commit
8cbe8a9
Β·
verified Β·
1 Parent(s): 128c0e8

Update upload script: support all checkpoint-* folders per expert, upload as branches + tags

Browse files
Files changed (1) hide show
  1. upload_classifiers.py +277 -122
upload_classifiers.py CHANGED
@@ -2,25 +2,38 @@
2
  Upload trained expert classifier checkpoints to the Hugging Face Hub.
3
 
4
  Scans for local checkpoint folders produced by train_expert_classifier.py
5
- and uploads each one to its corresponding Hub repo.
6
-
7
- Expected local structure (from training):
8
- ./cls-expert-reentrancy/best_model/
9
- adapter_config.json
10
- adapter_model.safetensors
11
- tokenizer.json
12
- tokenizer_config.json
13
- ...
14
- ./cls-expert-access-control/best_model/
 
 
 
 
 
 
 
 
15
  ...
16
 
17
- Each folder gets uploaded to:
18
  jhsu12/solidity-vuln-cls-reentrancy-v1
19
  jhsu12/solidity-vuln-cls-access-control-v1
20
  ...
21
 
 
 
 
 
 
22
  Usage:
23
- # Auto-detect and upload all expert folders found in current directory:
24
  python upload_classifiers.py
25
 
26
  # Upload from a custom base directory:
@@ -29,18 +42,26 @@ Usage:
29
  # Upload only specific experts:
30
  python upload_classifiers.py --experts reentrancy access-control
31
 
 
 
 
32
  # Dry run β€” show what would be uploaded without actually doing it:
33
  python upload_classifiers.py --dry_run
34
 
35
  # Upload to a different Hub namespace:
36
  python upload_classifiers.py --hub_namespace myorg
37
 
38
- # Upload specific folders by path (overrides auto-detection):
39
- python upload_classifiers.py --folder ./my-custom-checkpoint --hub_repo jhsu12/my-custom-model
 
 
 
 
40
  """
41
 
42
  import argparse
43
  import os
 
44
  import sys
45
  import json
46
  from huggingface_hub import HfApi, create_repo
@@ -64,9 +85,9 @@ IGNORE_PATTERNS = [
64
  "training_args*",
65
  "trainer_state*",
66
  "rng_state*",
67
- "*.pt", # Full optimizer states
68
- "global_step*", # Checkpoint step markers
69
- "runs/", # Tensorboard logs
70
  ]
71
 
72
 
@@ -92,18 +113,22 @@ def parse_args():
92
  help="Version suffix for Hub repo name (default: v1)"
93
  )
94
  parser.add_argument(
95
- "--subfolder", type=str, default="best_model",
96
- help="Subfolder inside each expert dir containing the checkpoint (default: best_model). "
97
- "Set to empty string if checkpoint is directly in the expert folder."
98
  )
99
  parser.add_argument(
100
  "--folder", type=str, default=None,
101
- help="Upload a single specific folder (overrides auto-detection)"
 
102
  )
103
  parser.add_argument(
104
  "--hub_repo", type=str, default=None,
105
  help="Target Hub repo ID for --folder mode (e.g. jhsu12/my-model)"
106
  )
 
 
 
 
107
  parser.add_argument(
108
  "--private", action="store_true", default=False,
109
  help="Create Hub repos as private (default: public)"
@@ -119,30 +144,59 @@ def parse_args():
119
  return parser.parse_args()
120
 
121
 
122
- def find_checkpoint_folders(base_dir, subfolder="best_model", filter_experts=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  """
124
- Scan base_dir for cls-expert-* folders and return list of
125
- (slug, expert_name, folder_path) tuples.
126
  """
127
  found = []
128
  base_dir = os.path.abspath(base_dir)
129
 
130
  for slug, expert_name in EXPERTS.items():
 
 
 
131
  dir_name = f"cls-expert-{slug}"
132
  expert_dir = os.path.join(base_dir, dir_name)
133
 
134
- if subfolder:
135
- checkpoint_dir = os.path.join(expert_dir, subfolder)
136
- else:
137
- checkpoint_dir = expert_dir
138
-
139
- if filter_experts and slug not in filter_experts:
140
  continue
141
 
142
- if not os.path.isdir(checkpoint_dir):
143
- continue
144
 
145
- found.append((slug, expert_name, checkpoint_dir))
 
 
 
 
 
 
 
146
 
147
  return found
148
 
@@ -154,14 +208,26 @@ def validate_checkpoint(folder_path):
154
  if not os.path.isfile(os.path.join(folder_path, f)):
155
  missing.append(f)
156
 
157
- # List what's actually there
158
  files = os.listdir(folder_path) if os.path.isdir(folder_path) else []
159
-
160
  return missing, files
161
 
162
 
163
- def create_model_card(expert_name, slug, hub_repo_id, base_model="Qwen/Qwen2.5-Coder-3B-Instruct"):
 
164
  """Generate a README.md model card for the Hub repo."""
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  return f"""---
166
  library_name: peft
167
  base_model: {base_model}
@@ -188,7 +254,7 @@ Binary classifier that detects **{expert_name}** vulnerabilities in Solidity sma
188
  - **Task**: Sequence Classification (2 labels: safe / vulnerable)
189
  - **LoRA rank**: 16, targeting q_proj, k_proj, v_proj, o_proj
190
  - **Classification head**: `modules_to_save=["score"]`
191
-
192
  ## Usage
193
 
194
  ```python
@@ -196,7 +262,6 @@ from transformers import AutoTokenizer, AutoModelForSequenceClassification, Bits
196
  from peft import PeftModel
197
  import torch
198
 
199
- # Load
200
  base_model = "{base_model}"
201
  bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
202
  bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True)
@@ -209,7 +274,6 @@ model.eval()
209
 
210
  tokenizer = AutoTokenizer.from_pretrained("{hub_repo_id}", trust_remote_code=True)
211
 
212
- # Classify
213
  code = "pragma solidity ^0.8.0; contract Example {{ ... }}"
214
  inputs = tokenizer(code, return_tensors="pt", truncation=True, max_length=1536).to(model.device)
215
  with torch.no_grad():
@@ -238,62 +302,135 @@ This is one of 5 expert classifiers in the
238
  """
239
 
240
 
241
- def upload_checkpoint(api, folder_path, hub_repo_id, expert_name, slug,
242
- private=False, commit_message=None, dry_run=False):
243
- """Upload a single checkpoint folder to the Hub."""
244
-
245
- print(f"\n{'─' * 60}")
246
- print(f" πŸ“¦ {expert_name}")
247
- print(f" Local: {folder_path}")
248
- print(f" Hub: https://huggingface.co/{hub_repo_id}")
249
-
250
- # Validate
251
  missing, files = validate_checkpoint(folder_path)
252
- print(f" Files: {len(files)} ({', '.join(files[:5])}{'...' if len(files) > 5 else ''})")
253
 
254
  if missing:
255
- print(f" ⚠️ Missing expected files: {missing}")
256
- print(f" Skipping β€” this doesn't look like a valid classifier checkpoint.")
257
  return False
258
 
259
  if dry_run:
260
- print(f" πŸ” DRY RUN β€” would upload {len(files)} files to {hub_repo_id}")
261
  return True
262
 
263
- # Create repo if needed
264
- try:
265
- create_repo(hub_repo_id, private=private, exist_ok=True)
266
- print(f" βœ… Repo ready")
267
- except Exception as e:
268
- print(f" ❌ Failed to create repo: {e}")
269
- return False
270
-
271
- # Generate and save model card
272
- readme_content = create_model_card(expert_name, slug, hub_repo_id)
273
- readme_path = os.path.join(folder_path, "README.md")
274
- readme_existed = os.path.exists(readme_path)
275
- with open(readme_path, "w") as f:
276
- f.write(readme_content)
277
-
278
- # Upload
279
- msg = commit_message or f"Upload {expert_name} classifier (QLoRA, Qwen2.5-Coder-3B)"
280
  try:
281
  api.upload_folder(
282
  folder_path=folder_path,
283
  repo_id=hub_repo_id,
 
284
  ignore_patterns=IGNORE_PATTERNS,
285
- commit_message=msg,
286
  )
287
- print(f" πŸš€ Uploaded to https://huggingface.co/{hub_repo_id}")
288
  except Exception as e:
289
- print(f" ❌ Upload failed: {e}")
290
  return False
291
- finally:
292
- # Clean up generated README if it didn't exist before
293
- if not readme_existed and os.path.exists(readme_path):
294
- os.remove(readme_path)
295
 
296
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
 
299
  def main():
@@ -304,7 +441,7 @@ def main():
304
  print(" Upload Expert Classifier Checkpoints to Hugging Face Hub")
305
  print("=" * 60)
306
 
307
- # ── Mode 1: Upload a single specific folder ──────────────────────────────
308
  if args.folder:
309
  if not args.hub_repo:
310
  print("❌ --hub_repo is required when using --folder")
@@ -315,77 +452,95 @@ def main():
315
  print(f"❌ Folder not found: {folder}")
316
  sys.exit(1)
317
 
318
- # Try to guess expert name from folder
319
  slug = os.path.basename(folder.rstrip("/"))
320
  expert_name = slug.replace("-", " ").replace("cls expert ", "").title()
321
 
322
- success = upload_checkpoint(
323
- api, folder, args.hub_repo, expert_name, slug,
324
- private=args.private, commit_message=args.commit_message,
325
- dry_run=args.dry_run,
 
 
 
 
 
 
 
 
 
 
326
  )
327
- sys.exit(0 if success else 1)
 
328
 
329
  # ── Mode 2: Auto-detect expert folders ────────────────────────────────────
330
  base_dir = os.path.abspath(args.base_dir)
331
  print(f"\nπŸ” Scanning: {base_dir}")
332
- print(f" Subfolder: {args.subfolder or '(root)'}")
333
 
334
- checkpoints = find_checkpoint_folders(
335
- base_dir,
336
- subfolder=args.subfolder,
337
- filter_experts=args.experts,
338
- )
339
 
340
- if not checkpoints:
341
- print(f"\n❌ No checkpoint folders found!")
342
  print(f"\n Expected structure:")
343
  print(f" {base_dir}/")
344
  for slug in EXPERTS:
345
- sub = f"/{args.subfolder}" if args.subfolder else ""
346
- print(f" cls-expert-{slug}{sub}/")
347
- print(f" adapter_config.json")
348
- print(f" adapter_model.safetensors")
349
- print(f" tokenizer.json")
350
- print(f" ...")
 
351
  print(f"\n Tips:")
352
  print(f" β€’ Run train_expert_classifier.py first to generate checkpoints")
353
- print(f" β€’ Use --base_dir to point to the directory containing the folders")
354
- print(f" β€’ Use --subfolder '' if checkpoints are in the expert folder root")
355
  sys.exit(1)
356
 
357
- print(f"\n Found {len(checkpoints)} expert checkpoint(s):")
358
- for slug, name, path in checkpoints:
359
- print(f" β€’ {name} β†’ {path}")
 
 
 
360
 
361
- # Upload each
362
- succeeded = 0
363
- failed = 0
 
 
 
364
 
365
- for slug, expert_name, folder_path in checkpoints:
366
  hub_repo_id = f"{args.hub_namespace}/solidity-vuln-cls-{slug}-{args.version}"
367
 
368
- success = upload_checkpoint(
369
- api, folder_path, hub_repo_id, expert_name, slug,
370
- private=args.private, commit_message=args.commit_message,
371
- dry_run=args.dry_run,
372
  )
373
- if success:
374
- succeeded += 1
375
- else:
376
- failed += 1
377
 
378
- # Summary
379
  print(f"\n{'=' * 60}")
380
  action = "Would upload" if args.dry_run else "Uploaded"
381
- print(f" {action}: {succeeded}/{succeeded + failed}")
382
- if failed:
383
- print(f" Failed: {failed}")
384
- if not args.dry_run and succeeded > 0:
 
385
  print(f"\n Hub repos:")
386
- for slug, name, _ in checkpoints:
387
  repo_id = f"{args.hub_namespace}/solidity-vuln-cls-{slug}-{args.version}"
 
388
  print(f" https://huggingface.co/{repo_id}")
 
 
 
 
 
 
389
  print(f"{'=' * 60}")
390
 
391
 
 
2
  Upload trained expert classifier checkpoints to the Hugging Face Hub.
3
 
4
  Scans for local checkpoint folders produced by train_expert_classifier.py
5
+ and uploads ALL checkpoints (checkpoint-100, checkpoint-200, etc.) for each
6
+ expert to a single Hub repo. Each checkpoint is uploaded as a tagged revision
7
+ so you can load any one by name.
8
+
9
+ Expected local structure (from HF Trainer with save_strategy="epoch"):
10
+ ./cls-expert-reentrancy/
11
+ checkpoint-50/
12
+ adapter_config.json
13
+ adapter_model.safetensors
14
+ tokenizer.json
15
+ ...
16
+ checkpoint-100/
17
+ ...
18
+ checkpoint-150/
19
+ ...
20
+ ./cls-expert-access-control/
21
+ checkpoint-50/
22
+ ...
23
  ...
24
 
25
+ Each expert folder gets a single Hub repo:
26
  jhsu12/solidity-vuln-cls-reentrancy-v1
27
  jhsu12/solidity-vuln-cls-access-control-v1
28
  ...
29
 
30
+ With tagged revisions for each checkpoint:
31
+ checkpoint-50, checkpoint-100, checkpoint-150, ...
32
+
33
+ The LAST checkpoint (highest step number) is also uploaded to `main`.
34
+
35
  Usage:
36
+ # Auto-detect and upload all expert folders + all checkpoints:
37
  python upload_classifiers.py
38
 
39
  # Upload from a custom base directory:
 
42
  # Upload only specific experts:
43
  python upload_classifiers.py --experts reentrancy access-control
44
 
45
+ # Upload only the latest checkpoint per expert (skip older ones):
46
+ python upload_classifiers.py --latest_only
47
+
48
  # Dry run β€” show what would be uploaded without actually doing it:
49
  python upload_classifiers.py --dry_run
50
 
51
  # Upload to a different Hub namespace:
52
  python upload_classifiers.py --hub_namespace myorg
53
 
54
+ # Upload a single arbitrary folder:
55
+ python upload_classifiers.py --folder ./my-checkpoint --hub_repo jhsu12/my-model
56
+
57
+ # If your checkpoints don't follow the cls-expert-* naming,
58
+ # upload a whole expert directory with all its checkpoint-* subfolders:
59
+ python upload_classifiers.py --folder ./my-expert-dir --hub_repo jhsu12/my-model --all_checkpoints
60
  """
61
 
62
  import argparse
63
  import os
64
+ import re
65
  import sys
66
  import json
67
  from huggingface_hub import HfApi, create_repo
 
85
  "training_args*",
86
  "trainer_state*",
87
  "rng_state*",
88
+ "*.pt",
89
+ "global_step*",
90
+ "runs/",
91
  ]
92
 
93
 
 
113
  help="Version suffix for Hub repo name (default: v1)"
114
  )
115
  parser.add_argument(
116
+ "--latest_only", action="store_true", default=False,
117
+ help="Only upload the latest checkpoint (highest step) per expert"
 
118
  )
119
  parser.add_argument(
120
  "--folder", type=str, default=None,
121
+ help="Upload a single specific folder (overrides auto-detection). "
122
+ "Can be a checkpoint dir or a parent dir containing checkpoint-* subdirs."
123
  )
124
  parser.add_argument(
125
  "--hub_repo", type=str, default=None,
126
  help="Target Hub repo ID for --folder mode (e.g. jhsu12/my-model)"
127
  )
128
+ parser.add_argument(
129
+ "--all_checkpoints", action="store_true", default=False,
130
+ help="When using --folder, scan it for checkpoint-* subdirs and upload all"
131
+ )
132
  parser.add_argument(
133
  "--private", action="store_true", default=False,
134
  help="Create Hub repos as private (default: public)"
 
144
  return parser.parse_args()
145
 
146
 
147
+ def find_checkpoints_in_dir(expert_dir):
148
+ """
149
+ Find all checkpoint-* subdirectories inside an expert folder.
150
+ Returns list of (step_number, folder_name, folder_path) sorted by step.
151
+ """
152
+ checkpoints = []
153
+ if not os.path.isdir(expert_dir):
154
+ return checkpoints
155
+
156
+ for name in os.listdir(expert_dir):
157
+ path = os.path.join(expert_dir, name)
158
+ if not os.path.isdir(path):
159
+ continue
160
+
161
+ # Match checkpoint-NNN pattern
162
+ match = re.match(r"checkpoint-(\d+)$", name)
163
+ if match:
164
+ step = int(match.group(1))
165
+ checkpoints.append((step, name, path))
166
+
167
+ # Sort by step number (ascending)
168
+ checkpoints.sort(key=lambda x: x[0])
169
+ return checkpoints
170
+
171
+
172
+ def find_all_experts(base_dir, filter_experts=None):
173
  """
174
+ Scan base_dir for cls-expert-* folders and find all checkpoints in each.
175
+ Returns list of (slug, expert_name, expert_dir, checkpoints) tuples.
176
  """
177
  found = []
178
  base_dir = os.path.abspath(base_dir)
179
 
180
  for slug, expert_name in EXPERTS.items():
181
+ if filter_experts and slug not in filter_experts:
182
+ continue
183
+
184
  dir_name = f"cls-expert-{slug}"
185
  expert_dir = os.path.join(base_dir, dir_name)
186
 
187
+ if not os.path.isdir(expert_dir):
 
 
 
 
 
188
  continue
189
 
190
+ checkpoints = find_checkpoints_in_dir(expert_dir)
 
191
 
192
+ # Also check if the expert_dir itself is a valid checkpoint (no subdirs)
193
+ if not checkpoints:
194
+ missing, _ = validate_checkpoint(expert_dir)
195
+ if not missing:
196
+ checkpoints = [(0, ".", expert_dir)]
197
+
198
+ if checkpoints:
199
+ found.append((slug, expert_name, expert_dir, checkpoints))
200
 
201
  return found
202
 
 
208
  if not os.path.isfile(os.path.join(folder_path, f)):
209
  missing.append(f)
210
 
 
211
  files = os.listdir(folder_path) if os.path.isdir(folder_path) else []
 
212
  return missing, files
213
 
214
 
215
+ def create_model_card(expert_name, slug, hub_repo_id, checkpoint_tags,
216
+ base_model="Qwen/Qwen2.5-Coder-3B-Instruct"):
217
  """Generate a README.md model card for the Hub repo."""
218
+
219
+ tags_table = ""
220
+ if len(checkpoint_tags) > 1:
221
+ tags_table = "\n## Available Checkpoints\n\n"
222
+ tags_table += "Load a specific checkpoint with `revision=`:\n"
223
+ tags_table += "```python\n"
224
+ tags_table += f'model = PeftModel.from_pretrained(base, "{hub_repo_id}", revision="checkpoint-200")\n'
225
+ tags_table += "```\n\n"
226
+ tags_table += "| Tag | Step |\n|-----|------|\n"
227
+ for tag_name, step in checkpoint_tags:
228
+ is_main = " ← `main`" if step == checkpoint_tags[-1][1] else ""
229
+ tags_table += f"| `{tag_name}` | {step}{is_main} |\n"
230
+
231
  return f"""---
232
  library_name: peft
233
  base_model: {base_model}
 
254
  - **Task**: Sequence Classification (2 labels: safe / vulnerable)
255
  - **LoRA rank**: 16, targeting q_proj, k_proj, v_proj, o_proj
256
  - **Classification head**: `modules_to_save=["score"]`
257
+ {tags_table}
258
  ## Usage
259
 
260
  ```python
 
262
  from peft import PeftModel
263
  import torch
264
 
 
265
  base_model = "{base_model}"
266
  bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
267
  bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True)
 
274
 
275
  tokenizer = AutoTokenizer.from_pretrained("{hub_repo_id}", trust_remote_code=True)
276
 
 
277
  code = "pragma solidity ^0.8.0; contract Example {{ ... }}"
278
  inputs = tokenizer(code, return_tensors="pt", truncation=True, max_length=1536).to(model.device)
279
  with torch.no_grad():
 
302
  """
303
 
304
 
305
+ def upload_single_checkpoint(api, folder_path, hub_repo_id, tag_name,
306
+ commit_message, revision="main", dry_run=False):
307
+ """Upload one checkpoint folder to a specific branch/revision of a Hub repo."""
 
 
 
 
 
 
 
308
  missing, files = validate_checkpoint(folder_path)
 
309
 
310
  if missing:
311
+ print(f" ⚠️ Missing files: {missing} β€” skipping")
 
312
  return False
313
 
314
  if dry_run:
315
+ print(f" πŸ” DRY RUN β€” {len(files)} files β†’ {revision}")
316
  return True
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  try:
319
  api.upload_folder(
320
  folder_path=folder_path,
321
  repo_id=hub_repo_id,
322
+ revision=revision,
323
  ignore_patterns=IGNORE_PATTERNS,
324
+ commit_message=commit_message,
325
  )
326
+ return True
327
  except Exception as e:
328
+ print(f" ❌ Upload failed: {e}")
329
  return False
 
 
 
 
330
 
331
+
332
+ def upload_expert(api, slug, expert_name, checkpoints, hub_repo_id,
333
+ private=False, latest_only=False, commit_message=None,
334
+ dry_run=False):
335
+ """
336
+ Upload all checkpoints for a single expert.
337
+ - Latest checkpoint β†’ main branch
338
+ - Each checkpoint β†’ tagged revision (checkpoint-NNN)
339
+ """
340
+ print(f"\n{'━' * 60}")
341
+ print(f" πŸ“¦ {expert_name}")
342
+ print(f" Hub: https://huggingface.co/{hub_repo_id}")
343
+ print(f" Checkpoints: {len(checkpoints)} found")
344
+
345
+ for step, name, path in checkpoints:
346
+ _, files = validate_checkpoint(path)
347
+ print(f" β€’ {name} ({len(files)} files)")
348
+
349
+ if latest_only:
350
+ checkpoints = [checkpoints[-1]] # Keep only the highest step
351
+ print(f" --latest_only: uploading only {checkpoints[0][1]}")
352
+
353
+ # Create repo
354
+ if not dry_run:
355
+ try:
356
+ create_repo(hub_repo_id, private=private, exist_ok=True)
357
+ print(f" βœ… Repo ready")
358
+ except Exception as e:
359
+ print(f" ❌ Failed to create repo: {e}")
360
+ return 0, len(checkpoints)
361
+
362
+ succeeded = 0
363
+ failed = 0
364
+
365
+ # Upload each checkpoint
366
+ for i, (step, name, path) in enumerate(checkpoints):
367
+ is_latest = (i == len(checkpoints) - 1)
368
+ tag_name = name # e.g. "checkpoint-100"
369
+
370
+ # Latest goes to main; all get a tag
371
+ if is_latest:
372
+ print(f"\n πŸš€ {name} (step {step}) β†’ main + tag '{tag_name}'")
373
+
374
+ # Upload to main
375
+ msg = commit_message or f"Upload {expert_name} classifier β€” {name} (latest)"
376
+ ok = upload_single_checkpoint(
377
+ api, path, hub_repo_id, tag_name, msg,
378
+ revision="main", dry_run=dry_run,
379
+ )
380
+
381
+ if ok:
382
+ # Also create model card on main
383
+ if not dry_run:
384
+ checkpoint_tags = [(ckpt_name, s) for s, ckpt_name, _ in checkpoints]
385
+ readme = create_model_card(expert_name, slug, hub_repo_id, checkpoint_tags)
386
+ try:
387
+ api.upload_file(
388
+ path_or_fileobj=readme.encode("utf-8"),
389
+ path_in_repo="README.md",
390
+ repo_id=hub_repo_id,
391
+ commit_message=f"Add model card for {expert_name}",
392
+ )
393
+ except Exception:
394
+ pass # Non-fatal
395
+
396
+ # Create tag for this checkpoint
397
+ if not dry_run and name != ".":
398
+ try:
399
+ api.create_tag(
400
+ hub_repo_id, tag=tag_name,
401
+ tag_message=f"{expert_name} β€” {name} (step {step})",
402
+ )
403
+ print(f" 🏷️ Tagged as '{tag_name}'")
404
+ except Exception:
405
+ pass # Tag may already exist
406
+
407
+ succeeded += 1
408
+ else:
409
+ failed += 1
410
+ else:
411
+ print(f"\n πŸ“€ {name} (step {step}) β†’ branch '{tag_name}'")
412
+
413
+ # Create a branch for this checkpoint
414
+ if not dry_run:
415
+ try:
416
+ api.create_branch(hub_repo_id, branch=tag_name)
417
+ except Exception:
418
+ pass # Branch may already exist
419
+
420
+ msg = commit_message or f"Upload {expert_name} classifier β€” {name}"
421
+ ok = upload_single_checkpoint(
422
+ api, path, hub_repo_id, tag_name, msg,
423
+ revision=tag_name, dry_run=dry_run,
424
+ )
425
+
426
+ if ok:
427
+ succeeded += 1
428
+ if not dry_run:
429
+ print(f" βœ… Uploaded to branch '{tag_name}'")
430
+ else:
431
+ failed += 1
432
+
433
+ return succeeded, failed
434
 
435
 
436
  def main():
 
441
  print(" Upload Expert Classifier Checkpoints to Hugging Face Hub")
442
  print("=" * 60)
443
 
444
+ # ── Mode 1: Upload a single folder / directory ────────────────────────────
445
  if args.folder:
446
  if not args.hub_repo:
447
  print("❌ --hub_repo is required when using --folder")
 
452
  print(f"❌ Folder not found: {folder}")
453
  sys.exit(1)
454
 
 
455
  slug = os.path.basename(folder.rstrip("/"))
456
  expert_name = slug.replace("-", " ").replace("cls expert ", "").title()
457
 
458
+ if args.all_checkpoints:
459
+ # Treat folder as parent dir containing checkpoint-* subdirs
460
+ checkpoints = find_checkpoints_in_dir(folder)
461
+ if not checkpoints:
462
+ print(f"❌ No checkpoint-* subdirectories found in {folder}")
463
+ sys.exit(1)
464
+ else:
465
+ # Treat folder as a single checkpoint
466
+ checkpoints = [(0, os.path.basename(folder), folder)]
467
+
468
+ ok, fail = upload_expert(
469
+ api, slug, expert_name, checkpoints, args.hub_repo,
470
+ private=args.private, latest_only=args.latest_only,
471
+ commit_message=args.commit_message, dry_run=args.dry_run,
472
  )
473
+ print(f"\n Uploaded: {ok}, Failed: {fail}")
474
+ sys.exit(0 if fail == 0 else 1)
475
 
476
  # ── Mode 2: Auto-detect expert folders ────────────────────────────────────
477
  base_dir = os.path.abspath(args.base_dir)
478
  print(f"\nπŸ” Scanning: {base_dir}")
 
479
 
480
+ experts = find_all_experts(base_dir, filter_experts=args.experts)
 
 
 
 
481
 
482
+ if not experts:
483
+ print(f"\n❌ No expert checkpoint folders found!")
484
  print(f"\n Expected structure:")
485
  print(f" {base_dir}/")
486
  for slug in EXPERTS:
487
+ print(f" cls-expert-{slug}/")
488
+ print(f" checkpoint-50/")
489
+ print(f" adapter_config.json")
490
+ print(f" adapter_model.safetensors")
491
+ print(f" ...")
492
+ print(f" checkpoint-100/")
493
+ print(f" ...")
494
  print(f"\n Tips:")
495
  print(f" β€’ Run train_expert_classifier.py first to generate checkpoints")
496
+ print(f" β€’ Use --base_dir to point to the parent directory")
497
+ print(f" β€’ Use --folder for a single non-standard directory")
498
  sys.exit(1)
499
 
500
+ # Summary of what was found
501
+ total_ckpts = sum(len(ckpts) for _, _, _, ckpts in experts)
502
+ print(f"\n Found {len(experts)} expert(s) with {total_ckpts} total checkpoint(s):")
503
+ for slug, name, expert_dir, ckpts in experts:
504
+ steps = [str(s) for s, _, _ in ckpts]
505
+ print(f" β€’ {name}: {len(ckpts)} checkpoints (steps: {', '.join(steps)})")
506
 
507
+ if args.latest_only:
508
+ print(f"\n --latest_only: will upload only the latest checkpoint per expert")
509
+
510
+ # Upload each expert
511
+ total_ok = 0
512
+ total_fail = 0
513
 
514
+ for slug, expert_name, expert_dir, checkpoints in experts:
515
  hub_repo_id = f"{args.hub_namespace}/solidity-vuln-cls-{slug}-{args.version}"
516
 
517
+ ok, fail = upload_expert(
518
+ api, slug, expert_name, checkpoints, hub_repo_id,
519
+ private=args.private, latest_only=args.latest_only,
520
+ commit_message=args.commit_message, dry_run=args.dry_run,
521
  )
522
+ total_ok += ok
523
+ total_fail += fail
 
 
524
 
525
+ # Final summary
526
  print(f"\n{'=' * 60}")
527
  action = "Would upload" if args.dry_run else "Uploaded"
528
+ print(f" {action}: {total_ok} checkpoint(s) across {len(experts)} expert(s)")
529
+ if total_fail:
530
+ print(f" Failed: {total_fail}")
531
+
532
+ if not args.dry_run and total_ok > 0:
533
  print(f"\n Hub repos:")
534
+ for slug, name, _, ckpts in experts:
535
  repo_id = f"{args.hub_namespace}/solidity-vuln-cls-{slug}-{args.version}"
536
+ ckpt_names = [cn for _, cn, _ in ckpts]
537
  print(f" https://huggingface.co/{repo_id}")
538
+ if len(ckpt_names) > 1 and not args.latest_only:
539
+ print(f" Branches: {', '.join(ckpt_names)}")
540
+ print(f" main = {ckpt_names[-1]} (latest)")
541
+
542
+ print(f"\n To load a specific checkpoint:")
543
+ print(f' PeftModel.from_pretrained(base, "jhsu12/solidity-vuln-cls-reentrancy-v1", revision="checkpoint-100")')
544
  print(f"{'=' * 60}")
545
 
546