917eedffcf
Main / Python 3.11 - Docs (push) Has been cancelled
Main / Python 3.11 - Build (push) Has been cancelled
Main / Python 3.11 - Lint (push) Has been cancelled
Main / Python 3.11 - Style (push) Has been cancelled
Main / Python 3.11 - Test (push) Has been cancelled
Main / GPU CI (push) Has been cancelled
Main / Release (push) Has been cancelled
Main / Build and Push Docker Images (push) Has been cancelled
622 lines
25 KiB
Python
Executable File
622 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Prepares OlmOCR checkpoints for deployment by:
|
|
1. Validating the model architecture
|
|
2. Copying model files to destination (disk or S3)
|
|
3. Downloading required tokenizer files from Hugging Face
|
|
|
|
Supports model souping (averaging weights of multiple checkpoints).
|
|
|
|
Usage:
|
|
python prepare_olmocr_checkpoint.py <source_path> <destination_path>
|
|
|
|
source_path: Path to checkpoint (local or S3)
|
|
destination_path: Where to save prepared checkpoint (local or S3)
|
|
|
|
For souping multiple checkpoints:
|
|
python prepare_olmocr_checkpoint.py <source1> <source2> ... <destination>
|
|
|
|
This will average the weights of all sources and prepare the souped checkpoint.
|
|
|
|
Examples:
|
|
# Single local to local
|
|
python prepare_olmocr_checkpoint.py /path/to/checkpoint /path/to/output
|
|
|
|
# Souping multiple S3 to S3
|
|
python prepare_olmocr_checkpoint.py s3://bucket/ckpt1 s3://bucket/ckpt2 s3://bucket/souped
|
|
|
|
# Mixed souping
|
|
python prepare_olmocr_checkpoint.py s3://bucket/ckpt1 /local/ckpt2 s3://bucket/souped
|
|
"""
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
import fnmatch
|
|
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from typing import Optional
|
|
|
|
import boto3
|
|
import requests
|
|
import torch
|
|
from botocore.exceptions import ClientError
|
|
from smart_open import smart_open
|
|
from tqdm import tqdm
|
|
from transformers import (
|
|
AutoConfig,
|
|
Qwen2_5_VLForConditionalGeneration,
|
|
Qwen2VLForConditionalGeneration,
|
|
)
|
|
|
|
try:
|
|
from safetensors.torch import load_file, save_file
|
|
except ImportError:
|
|
raise ImportError("Please install safetensors: pip install safetensors")
|
|
|
|
from olmocr.s3_utils import parse_s3_path
|
|
|
|
# Hugging Face model IDs for tokenizer files
|
|
HF_MODEL_IDS = {
|
|
"Qwen2VLForConditionalGeneration": "Qwen/Qwen2-VL-7B-Instruct",
|
|
"Qwen2_5_VLForConditionalGeneration": "Qwen/Qwen2.5-VL-7B-Instruct",
|
|
"Qwen3VLForConditionalGeneration": "Qwen/Qwen3-VL-8B-Instruct",
|
|
}
|
|
|
|
# Required tokenizer files to download from Hugging Face
|
|
TOKENIZER_FILES = ["chat_template.json", "merges.txt", "preprocessor_config.json", "tokenizer.json", "tokenizer_config.json", "vocab.json"]
|
|
|
|
# Supported model architectures
|
|
SUPPORTED_ARCHITECTURES = ["Qwen2VLForConditionalGeneration", "Qwen2_5_VLForConditionalGeneration", "Qwen3VLForConditionalGeneration"]
|
|
|
|
# Map architectures to corresponding model classes
|
|
MODEL_CLASS_MAP = {
|
|
"Qwen2VLForConditionalGeneration": Qwen2VLForConditionalGeneration,
|
|
"Qwen2_5_VLForConditionalGeneration": Qwen2_5_VLForConditionalGeneration,
|
|
}
|
|
|
|
# Files to exclude from copying (training-related files)
|
|
# Supports exact matches and glob patterns
|
|
EXCLUDED_FILES = {"optimizer.pt", "scheduler.pt", "rng_state.pth", "trainer_state.json", "training_args.bin", "*.pt", "*.pth"}
|
|
|
|
s3_client = boto3.client("s3")
|
|
|
|
|
|
def should_exclude_file(filename: str) -> bool:
|
|
"""Check if a file should be excluded based on EXCLUDED_FILES patterns."""
|
|
for pattern in EXCLUDED_FILES:
|
|
if fnmatch.fnmatch(filename, pattern):
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_s3_path(path: str) -> bool:
|
|
"""Check if a path is an S3 path."""
|
|
return path.startswith("s3://")
|
|
|
|
|
|
def join_path(base: str, *parts: str) -> str:
|
|
"""Join paths for local and S3-style URIs."""
|
|
if not parts:
|
|
return base
|
|
if is_s3_path(base):
|
|
cleaned = [base.rstrip("/")]
|
|
for part in parts:
|
|
cleaned.append(part.strip("/"))
|
|
return "/".join(segment for segment in cleaned if segment)
|
|
return os.path.join(base, *parts)
|
|
|
|
|
|
def load_json_if_exists(path: str) -> Optional[dict]:
|
|
"""Load JSON from a path if it exists, otherwise return None."""
|
|
try:
|
|
with smart_open(path, "r") as handle:
|
|
return json.load(handle)
|
|
except FileNotFoundError:
|
|
return None
|
|
except ClientError as exc:
|
|
error_code = exc.response.get("Error", {}).get("Code")
|
|
if error_code in {"NoSuchKey", "404"}:
|
|
return None
|
|
raise
|
|
except OSError as exc:
|
|
# Handle S3 NoSuchKey errors that come through as OSError
|
|
exc_str = str(exc)
|
|
if "No such file" in exc_str:
|
|
return None
|
|
if "NoSuchKey" in exc_str:
|
|
return None
|
|
if "The specified key does not exist" in exc_str:
|
|
return None
|
|
raise
|
|
except Exception as exc:
|
|
# Catch any other S3-related errors for missing files
|
|
exc_str = str(exc)
|
|
if "NoSuchKey" in exc_str or "specified key does not exist" in exc_str.lower():
|
|
return None
|
|
raise
|
|
|
|
|
|
def load_adapter_config(source_path: str) -> Optional[dict]:
|
|
"""Return the LoRA adapter configuration if present for the given source."""
|
|
adapter_config_path = join_path(source_path, "adapter_config.json")
|
|
return load_json_if_exists(adapter_config_path)
|
|
|
|
|
|
def download_file_from_hf(filename: str, destination_dir: str, hf_base_url: str) -> None:
|
|
"""Download a file from Hugging Face model repository."""
|
|
url = f"{hf_base_url}/{filename}"
|
|
local_path = os.path.join(destination_dir, filename)
|
|
|
|
print(f"Downloading {filename} from Hugging Face...")
|
|
response = requests.get(url, stream=True)
|
|
response.raise_for_status()
|
|
|
|
with open(local_path, "wb") as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
|
|
print(f"Downloaded {filename}")
|
|
|
|
|
|
def detect_checkpoint_architecture(config_path: str) -> str:
|
|
"""Detect and validate the checkpoint architecture."""
|
|
print(f"Detecting checkpoint architecture from {config_path}...")
|
|
|
|
with smart_open(config_path, "r") as f:
|
|
config_data = json.load(f)
|
|
|
|
architectures = config_data.get("architectures", [])
|
|
|
|
# Find the supported architecture
|
|
detected_architecture = None
|
|
for arch in architectures:
|
|
if arch in SUPPORTED_ARCHITECTURES:
|
|
detected_architecture = arch
|
|
break
|
|
|
|
if not detected_architecture:
|
|
# Try to detect from model name
|
|
model_name = config_data.get("name_or_path", "")
|
|
if "Qwen2.5-VL" in model_name:
|
|
detected_architecture = "Qwen2_5_VLForConditionalGeneration"
|
|
elif "Qwen2-VL" in model_name:
|
|
detected_architecture = "Qwen2VLForConditionalGeneration"
|
|
elif "Qwen3-VL" in model_name:
|
|
detected_architecture = "Qwen3VLForConditionalGeneration"
|
|
else:
|
|
raise ValueError(f"No supported architecture found. Expected one of {SUPPORTED_ARCHITECTURES} " f"but found: {architectures}")
|
|
|
|
print(f"✓ Detected architecture: {detected_architecture}")
|
|
return detected_architecture
|
|
|
|
|
|
def copy_local_to_local(source_dir: str, dest_dir: str) -> None:
|
|
"""Copy files from local directory to local directory."""
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
|
|
# Get list of files to copy
|
|
files_to_copy = []
|
|
for root, _, files in os.walk(source_dir):
|
|
for file in files:
|
|
if should_exclude_file(file):
|
|
print(f"Skipping excluded file: {file}")
|
|
continue
|
|
src_path = os.path.join(root, file)
|
|
rel_path = os.path.relpath(src_path, source_dir)
|
|
files_to_copy.append((src_path, os.path.join(dest_dir, rel_path)))
|
|
|
|
print(f"Copying {len(files_to_copy)} files from {source_dir} to {dest_dir}...")
|
|
|
|
for src_path, dst_path in tqdm(files_to_copy, desc="Copying files"):
|
|
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
|
shutil.copy2(src_path, dst_path)
|
|
|
|
|
|
def download_file_from_s3(bucket: str, key: str, local_path: str) -> None:
|
|
"""Download a single file from S3."""
|
|
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
s3_client.download_file(bucket, key, local_path)
|
|
|
|
|
|
def upload_file_to_s3(local_path: str, bucket: str, key: str) -> None:
|
|
"""Upload a single file to S3."""
|
|
s3_client.upload_file(local_path, bucket, key)
|
|
|
|
|
|
def copy_s3_to_local(source_bucket: str, source_prefix: str, dest_dir: str) -> None:
|
|
"""Copy files from S3 to local directory."""
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
|
|
# List all objects in source
|
|
paginator = s3_client.get_paginator("list_objects_v2")
|
|
pages = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix)
|
|
|
|
download_tasks = []
|
|
for page in pages:
|
|
for obj in page.get("Contents", []):
|
|
key = obj["Key"]
|
|
if key.endswith("/"):
|
|
continue
|
|
|
|
filename = os.path.basename(key)
|
|
if should_exclude_file(filename):
|
|
print(f"Skipping excluded file: {filename}")
|
|
continue
|
|
|
|
rel_path = os.path.relpath(key, source_prefix)
|
|
local_path = os.path.join(dest_dir, rel_path)
|
|
download_tasks.append((source_bucket, key, local_path))
|
|
|
|
print(f"Downloading {len(download_tasks)} files from s3://{source_bucket}/{source_prefix} to {dest_dir}...")
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
futures = [executor.submit(download_file_from_s3, bucket, key, local_path) for bucket, key, local_path in download_tasks]
|
|
|
|
for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures), desc="Downloading"):
|
|
future.result()
|
|
|
|
|
|
def copy_local_to_s3(source_dir: str, dest_bucket: str, dest_prefix: str) -> None:
|
|
"""Copy files from local directory to S3."""
|
|
# Get list of files to upload
|
|
upload_tasks = []
|
|
for root, _, files in os.walk(source_dir):
|
|
for file in files:
|
|
if should_exclude_file(file):
|
|
print(f"Skipping excluded file: {file}")
|
|
continue
|
|
local_path = os.path.join(root, file)
|
|
rel_path = os.path.relpath(local_path, source_dir)
|
|
s3_key = os.path.join(dest_prefix, rel_path)
|
|
upload_tasks.append((local_path, dest_bucket, s3_key))
|
|
|
|
print(f"Uploading {len(upload_tasks)} files from {source_dir} to s3://{dest_bucket}/{dest_prefix}...")
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
futures = [executor.submit(upload_file_to_s3, local_path, bucket, key) for local_path, bucket, key in upload_tasks]
|
|
|
|
for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures), desc="Uploading"):
|
|
future.result()
|
|
|
|
|
|
def copy_s3_to_s3(source_bucket: str, source_prefix: str, dest_bucket: str, dest_prefix: str) -> None:
|
|
"""Copy files from S3 to S3."""
|
|
# List all objects in source
|
|
paginator = s3_client.get_paginator("list_objects_v2")
|
|
pages = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix)
|
|
|
|
copy_tasks = []
|
|
for page in pages:
|
|
for obj in page.get("Contents", []):
|
|
key = obj["Key"]
|
|
if key.endswith("/"):
|
|
continue
|
|
|
|
filename = os.path.basename(key)
|
|
if should_exclude_file(filename):
|
|
print(f"Skipping excluded file: {filename}")
|
|
continue
|
|
|
|
rel_path = os.path.relpath(key, source_prefix)
|
|
dest_key = os.path.join(dest_prefix, rel_path)
|
|
copy_source = {"Bucket": source_bucket, "Key": key}
|
|
copy_tasks.append((copy_source, dest_bucket, dest_key))
|
|
|
|
print(f"Copying {len(copy_tasks)} files from s3://{source_bucket}/{source_prefix} to s3://{dest_bucket}/{dest_prefix}...")
|
|
|
|
for copy_source, bucket, key in tqdm(copy_tasks, desc="Copying"):
|
|
s3_client.copy_object(CopySource=copy_source, Bucket=bucket, Key=key)
|
|
|
|
|
|
def get_weight_files(dir_path: str) -> list[str]:
|
|
"""Get list of weight files (full paths) in the directory."""
|
|
weight_files = []
|
|
for root, _, files in os.walk(dir_path):
|
|
for file in files:
|
|
full_path = os.path.join(root, file)
|
|
if (file.startswith("pytorch_model") and file.endswith(".bin")) or file.endswith(".safetensors"):
|
|
weight_files.append(full_path)
|
|
return weight_files
|
|
|
|
|
|
def merge_lora_adapter_checkpoint(adapter_dir: str, base_model_name: str, output_dir: str) -> str:
|
|
"""Merge a LoRA adapter into its base model and save the merged weights.
|
|
|
|
Returns:
|
|
The detected architecture string of the merged model.
|
|
"""
|
|
try:
|
|
from peft import PeftModel
|
|
except ImportError as exc: # pragma: no cover - optional dependency guard
|
|
raise ImportError("Merging LoRA adapters requires the `peft` package. Install it with `pip install peft`.") from exc
|
|
|
|
print(f"Merging LoRA adapter from {adapter_dir} with base model '{base_model_name}'...")
|
|
base_config = AutoConfig.from_pretrained(base_model_name, trust_remote_code=True)
|
|
|
|
architecture = None
|
|
for arch in base_config.architectures or []:
|
|
if arch in MODEL_CLASS_MAP:
|
|
architecture = arch
|
|
break
|
|
|
|
if architecture is None:
|
|
raise ValueError(
|
|
f"Base model '{base_model_name}' uses an unsupported architecture: {base_config.architectures}. "
|
|
f"Supported architectures: {SUPPORTED_ARCHITECTURES}"
|
|
)
|
|
|
|
model_class = MODEL_CLASS_MAP[architecture]
|
|
base_model = model_class.from_pretrained(
|
|
base_model_name,
|
|
trust_remote_code=True,
|
|
torch_dtype="auto",
|
|
)
|
|
|
|
lora_model = PeftModel.from_pretrained(base_model, adapter_dir, is_trainable=False)
|
|
merged_model = lora_model.merge_and_unload()
|
|
merged_model = merged_model.to("cpu")
|
|
|
|
if hasattr(merged_model, "config"):
|
|
merged_model.config._name_or_path = base_model_name
|
|
merged_model.config.base_model_name_or_path = base_model_name
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
merged_model.save_pretrained(output_dir)
|
|
print(f"✓ Saved merged model to {output_dir}")
|
|
|
|
# Explicit cleanup
|
|
del merged_model
|
|
del lora_model
|
|
del base_model
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
return architecture
|
|
|
|
|
|
def prepare_checkpoints(sources: list[str], dest_path: str) -> None:
|
|
"""Prepare OlmOCR checkpoint(s) for deployment, with support for souping."""
|
|
print(f"Preparing {'souped ' if len(sources) > 1 else ''}checkpoint from {len(sources)} source(s) to {dest_path}")
|
|
|
|
sources = [source.rstrip("/") for source in sources]
|
|
dest_path = dest_path.rstrip("/")
|
|
|
|
source_infos = []
|
|
for source in sources:
|
|
adapter_config = load_adapter_config(source)
|
|
if adapter_config is not None:
|
|
source_infos.append({"path": source, "is_lora": True, "adapter_config": adapter_config})
|
|
else:
|
|
config_path = join_path(source, "config.json")
|
|
arch = detect_checkpoint_architecture(config_path)
|
|
source_infos.append({"path": source, "is_lora": False, "architecture": arch})
|
|
|
|
num_lora_sources = sum(1 for info in source_infos if info["is_lora"])
|
|
final_architecture: Optional[str] = None
|
|
|
|
if num_lora_sources > 0:
|
|
if len(source_infos) > 1:
|
|
raise ValueError("LoRA adapter checkpoints can only be processed individually, not during souping.")
|
|
|
|
source_info = source_infos[0]
|
|
source_path = source_info["path"]
|
|
adapter_config = source_info["adapter_config"]
|
|
base_model_name = adapter_config.get("base_model_name_or_path")
|
|
if not base_model_name:
|
|
raise ValueError("adapter_config.json is missing 'base_model_name_or_path'; cannot merge LoRA adapter.")
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
adapter_local_dir = os.path.join(temp_dir, "adapter")
|
|
print("\nDownloading LoRA adapter locally for merging...")
|
|
if is_s3_path(source_path):
|
|
bucket, prefix = parse_s3_path(source_path)
|
|
copy_s3_to_local(bucket, prefix, adapter_local_dir)
|
|
else:
|
|
copy_local_to_local(source_path, adapter_local_dir)
|
|
|
|
merged_dir = os.path.join(temp_dir, "merged")
|
|
final_architecture = merge_lora_adapter_checkpoint(adapter_local_dir, base_model_name, merged_dir)
|
|
|
|
print("\nCopying merged model files to destination...")
|
|
if is_s3_path(dest_path):
|
|
dest_bucket, dest_prefix = parse_s3_path(dest_path)
|
|
copy_local_to_s3(merged_dir, dest_bucket, dest_prefix)
|
|
else:
|
|
copy_local_to_local(merged_dir, dest_path)
|
|
else:
|
|
architectures = [info["architecture"] for info in source_infos]
|
|
if len(set(architectures)) > 1:
|
|
raise ValueError("All sources must have the same architecture")
|
|
|
|
final_architecture = architectures[0]
|
|
|
|
if len(sources) == 1:
|
|
source_path = sources[0]
|
|
print("\nCopying model files...")
|
|
if is_s3_path(source_path) and is_s3_path(dest_path):
|
|
source_bucket, source_prefix = parse_s3_path(source_path)
|
|
dest_bucket, dest_prefix = parse_s3_path(dest_path)
|
|
copy_s3_to_s3(source_bucket, source_prefix, dest_bucket, dest_prefix)
|
|
elif is_s3_path(source_path) and not is_s3_path(dest_path):
|
|
source_bucket, source_prefix = parse_s3_path(source_path)
|
|
copy_s3_to_local(source_bucket, source_prefix, dest_path)
|
|
elif not is_s3_path(source_path) and is_s3_path(dest_path):
|
|
dest_bucket, dest_prefix = parse_s3_path(dest_path)
|
|
copy_local_to_s3(source_path, dest_bucket, dest_prefix)
|
|
else:
|
|
copy_local_to_local(source_path, dest_path)
|
|
else:
|
|
# Souping multiple checkpoints
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Download all sources to local temp dirs
|
|
source_temps = []
|
|
for i, source in enumerate(sources):
|
|
source_temp = os.path.join(temp_dir, f"source_{i}")
|
|
if is_s3_path(source):
|
|
bucket, prefix = parse_s3_path(source)
|
|
copy_s3_to_local(bucket, prefix, source_temp)
|
|
else:
|
|
copy_local_to_local(source, source_temp)
|
|
source_temps.append(source_temp)
|
|
|
|
first_source = source_temps[0]
|
|
|
|
# Get weight files
|
|
weight_full_paths = get_weight_files(first_source)
|
|
weight_rel_paths = [os.path.relpath(p, first_source) for p in weight_full_paths]
|
|
|
|
# Verify others have same weight files
|
|
for i in range(1, len(sources)):
|
|
other_dir = source_temps[i]
|
|
other_weights = [os.path.relpath(p, other_dir) for p in get_weight_files(other_dir)]
|
|
if set(other_weights) != set(weight_rel_paths):
|
|
raise ValueError(f"Source {sources[i]} has different weight files")
|
|
|
|
# Create souped_dir
|
|
souped_dir = os.path.join(temp_dir, "souped")
|
|
# Copy first source (including its weights, which will be overwritten)
|
|
copy_local_to_local(first_source, souped_dir)
|
|
|
|
# Average weights
|
|
for rel_path in tqdm(weight_rel_paths, desc="Averaging weight files"):
|
|
all_paths = [os.path.join(st, rel_path) for st in source_temps]
|
|
file_path = all_paths[0]
|
|
souped_path = os.path.join(souped_dir, rel_path)
|
|
os.makedirs(os.path.dirname(souped_path), exist_ok=True)
|
|
|
|
if file_path.endswith(".safetensors"):
|
|
sum_state = load_file(file_path, device="cpu")
|
|
# Store original dtypes for each tensor
|
|
original_dtypes = {k: v.dtype for k, v in sum_state.items()}
|
|
# Upconvert to at least fp32 for accurate averaging
|
|
for k in sum_state:
|
|
if sum_state[k].dtype in (torch.float16, torch.bfloat16):
|
|
sum_state[k] = sum_state[k].to(torch.float32)
|
|
|
|
for other_path in all_paths[1:]:
|
|
other_state = load_file(other_path, device="cpu")
|
|
if set(sum_state.keys()) != set(other_state.keys()):
|
|
raise ValueError(f"Key mismatch in {rel_path}")
|
|
for k in sum_state:
|
|
# Upconvert other state to match sum_state dtype
|
|
if other_state[k].dtype in (torch.float16, torch.bfloat16):
|
|
other_state[k] = other_state[k].to(torch.float32)
|
|
sum_state[k] += other_state[k]
|
|
del other_state
|
|
|
|
n = len(all_paths)
|
|
for k in sum_state:
|
|
sum_state[k] /= n
|
|
# Cast back to original dtype
|
|
sum_state[k] = sum_state[k].to(original_dtypes[k])
|
|
save_file(sum_state, souped_path)
|
|
elif file_path.endswith(".bin"):
|
|
sum_state = torch.load(file_path, map_location="cpu")
|
|
# Store original dtypes for each tensor
|
|
original_dtypes = {k: v.dtype for k, v in sum_state.items()}
|
|
# Upconvert to at least fp32 for accurate averaging
|
|
for k in sum_state:
|
|
if sum_state[k].dtype in (torch.float16, torch.bfloat16):
|
|
sum_state[k] = sum_state[k].to(torch.float32)
|
|
|
|
for other_path in all_paths[1:]:
|
|
other_state = torch.load(other_path, map_location="cpu")
|
|
if set(sum_state.keys()) != set(other_state.keys()):
|
|
raise ValueError(f"Key mismatch in {rel_path}")
|
|
for k in sum_state:
|
|
# Upconvert other state to match sum_state dtype
|
|
if other_state[k].dtype in (torch.float16, torch.bfloat16):
|
|
other_state[k] = other_state[k].to(torch.float32)
|
|
sum_state[k] += other_state[k]
|
|
del other_state
|
|
|
|
n = len(all_paths)
|
|
for k in sum_state:
|
|
sum_state[k] /= n
|
|
# Cast back to original dtype
|
|
sum_state[k] = sum_state[k].to(original_dtypes[k])
|
|
torch.save(sum_state, souped_path)
|
|
else:
|
|
print(f"Skipping unknown weight file: {rel_path}")
|
|
continue
|
|
|
|
# Now copy souped_dir to dest_path
|
|
print("\nCopying souped model files to destination...")
|
|
if is_s3_path(dest_path):
|
|
dest_bucket, dest_prefix = parse_s3_path(dest_path)
|
|
copy_local_to_s3(souped_dir, dest_bucket, dest_prefix)
|
|
else:
|
|
copy_local_to_local(souped_dir, dest_path)
|
|
|
|
if final_architecture is None:
|
|
raise ValueError("Unable to determine the architecture of the prepared checkpoint.")
|
|
|
|
hf_model_id = HF_MODEL_IDS[final_architecture]
|
|
hf_base_url = f"https://huggingface.co/{hf_model_id}/resolve/main"
|
|
print(f"Using HuggingFace model: {hf_model_id}")
|
|
|
|
# Download tokenizer files from Hugging Face
|
|
print("\nDownloading tokenizer files from Hugging Face...")
|
|
|
|
if is_s3_path(dest_path):
|
|
# Download to temp directory first, then upload to S3
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Download files
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor:
|
|
futures = [executor.submit(download_file_from_hf, filename, temp_dir, hf_base_url) for filename in TOKENIZER_FILES]
|
|
for future in concurrent.futures.as_completed(futures):
|
|
future.result()
|
|
|
|
# Upload to S3
|
|
dest_bucket, dest_prefix = parse_s3_path(dest_path)
|
|
upload_tasks = []
|
|
for filename in TOKENIZER_FILES:
|
|
local_path = os.path.join(temp_dir, filename)
|
|
s3_key = os.path.join(dest_prefix, filename)
|
|
upload_tasks.append((local_path, dest_bucket, s3_key))
|
|
|
|
print("Uploading tokenizer files to S3...")
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor:
|
|
futures = [executor.submit(upload_file_to_s3, local_path, bucket, key) for local_path, bucket, key in upload_tasks]
|
|
for future in concurrent.futures.as_completed(futures):
|
|
future.result()
|
|
else:
|
|
# Download directly to destination
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor:
|
|
futures = [executor.submit(download_file_from_hf, filename, dest_path, hf_base_url) for filename in TOKENIZER_FILES]
|
|
for future in concurrent.futures.as_completed(futures):
|
|
future.result()
|
|
|
|
print(f"\n✓ Successfully prepared checkpoint at {dest_path}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Prepare OlmOCR checkpoint for deployment",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__.split("Usage:")[1], # Use the docstring for epilog
|
|
)
|
|
parser.add_argument("paths", nargs="+", help="One or more source paths followed by destination path (local or S3)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if len(args.paths) < 2:
|
|
parser.error("At least one source and one destination required")
|
|
|
|
sources = args.paths[:-1]
|
|
destination = args.paths[-1]
|
|
|
|
try:
|
|
prepare_checkpoints(sources, destination)
|
|
except Exception as e:
|
|
print(f"\n❌ Error: {e}")
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|