|
15 | 15 | from sqlalchemy.ext.declarative import declarative_base |
16 | 16 | from sqlalchemy.orm import sessionmaker, relationship |
17 | 17 | from datetime import datetime |
| 18 | +from typing import Dict, List |
18 | 19 | import os |
19 | 20 | from backend.logging_config import get_logger |
20 | 21 |
|
@@ -140,6 +141,12 @@ async def init_db(): |
140 | 141 |
|
141 | 142 | # Migrate existing models to populate base_model_name |
142 | 143 | migrate_existing_models() |
| 144 | + |
| 145 | + # Migrate safetensors models: merge multiple rows per repo into single row |
| 146 | + try: |
| 147 | + migrate_safetensors_models_to_unified() |
| 148 | + except Exception as exc: |
| 149 | + logger.warning(f"Failed to migrate safetensors models: {exc}") |
143 | 150 |
|
144 | 151 |
|
145 | 152 | def migrate_existing_models(): |
@@ -199,6 +206,75 @@ def ensure_running_instance_runtime_column(): |
199 | 206 | logger.info("Added runtime_type column to running_instances table") |
200 | 207 |
|
201 | 208 |
|
| 209 | +def migrate_safetensors_models_to_unified(): |
| 210 | + """Migrate safetensors models: merge multiple Model rows per repo into a single row.""" |
| 211 | + db = SessionLocal() |
| 212 | + try: |
| 213 | + # Find all safetensors models grouped by huggingface_id |
| 214 | + safetensors_models = db.query(Model).filter( |
| 215 | + Model.model_format == "safetensors" |
| 216 | + ).all() |
| 217 | + |
| 218 | + # Group by huggingface_id |
| 219 | + by_repo: Dict[str, List[Model]] = {} |
| 220 | + for model in safetensors_models: |
| 221 | + hf_id = model.huggingface_id or "unknown" |
| 222 | + by_repo.setdefault(hf_id, []).append(model) |
| 223 | + |
| 224 | + merged_count = 0 |
| 225 | + for huggingface_id, models in by_repo.items(): |
| 226 | + if len(models) <= 1: |
| 227 | + continue # Already unified |
| 228 | + |
| 229 | + # Keep the first model, merge others into it |
| 230 | + primary = models[0] |
| 231 | + others = models[1:] |
| 232 | + |
| 233 | + # Aggregate file_size |
| 234 | + total_size = sum(m.file_size or 0 for m in models) |
| 235 | + if total_size: |
| 236 | + primary.file_size = total_size |
| 237 | + |
| 238 | + # Merge metadata: use most complete pipeline_tag, model_type, etc. |
| 239 | + for other in others: |
| 240 | + if not primary.pipeline_tag and other.pipeline_tag: |
| 241 | + primary.pipeline_tag = other.pipeline_tag |
| 242 | + if not primary.model_type and other.model_type: |
| 243 | + primary.model_type = other.model_type |
| 244 | + if not primary.base_model_name and other.base_model_name: |
| 245 | + primary.base_model_name = other.base_model_name |
| 246 | + # Use earliest downloaded_at |
| 247 | + if other.downloaded_at and (not primary.downloaded_at or other.downloaded_at < primary.downloaded_at): |
| 248 | + primary.downloaded_at = other.downloaded_at |
| 249 | + |
| 250 | + # Update RunningInstance records to point to primary model |
| 251 | + for other in others: |
| 252 | + instances = db.query(RunningInstance).filter( |
| 253 | + RunningInstance.model_id == other.id |
| 254 | + ).all() |
| 255 | + for instance in instances: |
| 256 | + instance.model_id = primary.id |
| 257 | + |
| 258 | + # Delete duplicate models |
| 259 | + for other in others: |
| 260 | + db.delete(other) |
| 261 | + |
| 262 | + merged_count += len(others) |
| 263 | + logger.info(f"Merged {len(others)} safetensors Model rows for {huggingface_id} into model_id={primary.id}") |
| 264 | + |
| 265 | + if merged_count > 0: |
| 266 | + db.commit() |
| 267 | + logger.info(f"Migration complete: merged {merged_count} safetensors Model rows") |
| 268 | + else: |
| 269 | + logger.debug("No safetensors Model rows to merge") |
| 270 | + |
| 271 | + except Exception as e: |
| 272 | + logger.error(f"Error migrating safetensors models: {e}") |
| 273 | + db.rollback() |
| 274 | + finally: |
| 275 | + db.close() |
| 276 | + |
| 277 | + |
202 | 278 | def ensure_pipeline_tag_column(): |
203 | 279 | """Ensure the models table stores pipeline tags.""" |
204 | 280 | inspector = inspect(engine) |
|
0 commit comments