Update main_medium.py
Browse files- main_medium.py +165 -39
main_medium.py
CHANGED
|
@@ -240,69 +240,148 @@ HF_BACKUP_PRIVATE = True
|
|
| 240 |
# "meilleur") existait juste avant le crash.
|
| 241 |
HF_BACKUP_LATEST_INTERVAL_SECONDS = 600 # 10 min
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
from huggingface_hub import HfApi
|
| 253 |
api = HfApi()
|
| 254 |
try:
|
| 255 |
-
api.create_repo(repo_id=
|
| 256 |
except Exception as e:
|
| 257 |
-
print(f"⚠️ Impossible de créer/vérifier le repo
|
| 258 |
-
f"La sauvegarde externe est désactivée pour cette session.")
|
| 259 |
return None
|
| 260 |
-
|
| 261 |
return api
|
| 262 |
|
| 263 |
|
| 264 |
-
def push_backup_to_hf(local_path: str, path_in_repo: str
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
| 268 |
if not HF_BACKUP_ENABLED or not os.path.exists(local_path):
|
| 269 |
return
|
| 270 |
-
|
|
|
|
|
|
|
| 271 |
if api is None:
|
| 272 |
return
|
| 273 |
try:
|
| 274 |
api.upload_file(
|
| 275 |
path_or_fileobj=local_path,
|
| 276 |
path_in_repo=path_in_repo,
|
| 277 |
-
repo_id=
|
| 278 |
-
repo_type=
|
| 279 |
)
|
| 280 |
size_mb = os.path.getsize(local_path) / 1e6
|
| 281 |
-
print(f" ↳ 🛟 Sauvegarde externe: '{path_in_repo}' poussé vers {
|
| 282 |
except Exception as e:
|
| 283 |
print(f" ↳ ⚠️ Échec de la sauvegarde externe de '{path_in_repo}' ({e}) — entraînement non interrompu.")
|
| 284 |
|
| 285 |
|
| 286 |
-
def try_restore_backup_from_hf(local_path: str, path_in_repo: str
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
| 291 |
if not HF_BACKUP_ENABLED or os.path.exists(local_path):
|
| 292 |
return False
|
|
|
|
| 293 |
try:
|
| 294 |
from huggingface_hub import hf_hub_download
|
| 295 |
-
downloaded = hf_hub_download(repo_id=HF_BACKUP_REPO_ID, filename=path_in_repo, repo_type="model")
|
| 296 |
-
import shutil
|
| 297 |
os.makedirs(os.path.dirname(local_path) or ".", exist_ok=True)
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
return True
|
| 301 |
except Exception as e:
|
| 302 |
-
print(f"ℹ️ Pas de sauvegarde externe utilisable pour '{path_in_repo}' ({e}).")
|
| 303 |
return False
|
| 304 |
|
| 305 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
# ============================================================================
|
| 307 |
# Configuration
|
| 308 |
# ============================================================================
|
|
@@ -362,6 +441,7 @@ EVAL_INTERVAL = 200 # Évaluation + sauvegarde du meilleur modèle tous l
|
|
| 362 |
GEN_INTERVAL = 400 # Génération d'un échantillon de contrôle tous les N steps
|
| 363 |
CHECKPOINT_PATH = os.path.join(_SAVE_DIR, "best_model_medium.pt")
|
| 364 |
LATEST_CHECKPOINT_PATH = os.path.join(_SAVE_DIR, "latest_model_medium.pt")
|
|
|
|
| 365 |
SEED = 1337
|
| 366 |
|
| 367 |
# Cible de tokens pour le pré-entraînement à grande échelle (modèle "Large").
|
|
@@ -1181,6 +1261,10 @@ def build_pretraining_corpus_bin(
|
|
| 1181 |
os.remove(bin_path)
|
| 1182 |
os.replace(alt_path, bin_path)
|
| 1183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1184 |
if os.path.exists(bin_path) and os.path.exists(meta_path):
|
| 1185 |
with open(meta_path, "r", encoding="utf-8") as f:
|
| 1186 |
meta = json.load(f)
|
|
@@ -1438,9 +1522,30 @@ def prepare_data_once() -> tuple[str, int]:
|
|
| 1438 |
bin_path, total_tokens = build_pretraining_corpus_bin(
|
| 1439 |
tokenizer, target_tokens=TARGET_PRETRAIN_TOKENS, extra_text_once=seed_text,
|
| 1440 |
)
|
|
|
|
|
|
|
| 1441 |
return bin_path, total_tokens
|
| 1442 |
|
| 1443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1444 |
def main_worker(rank: int, world_size: int, bin_path: str, total_tokens: int) -> None:
|
| 1445 |
|
| 1446 |
if world_size > 1:
|
|
@@ -1501,15 +1606,21 @@ def main_worker(rank: int, world_size: int, bin_path: str, total_tokens: int) ->
|
|
| 1501 |
# depuis la sauvegarde externe HF Hub (utile après une session Kaggle
|
| 1502 |
# perdue sans 'Save Version'), PUIS le comportement RESUME_FROM_CHECKPOINT
|
| 1503 |
# habituel sur le fichier local (potentiellement fraîchement restauré).
|
| 1504 |
-
if accelerator.is_main_process and RESUME_FROM_CHECKPOINT
|
| 1505 |
-
|
|
|
|
|
|
|
|
|
|
| 1506 |
if accelerator.num_processes > 1:
|
| 1507 |
accelerator.wait_for_everyone() # tous les rangs attendent la restauration éventuelle du rang 0
|
| 1508 |
|
| 1509 |
resumed_step = 0
|
| 1510 |
resumed_step_is_approximate = False
|
| 1511 |
-
|
| 1512 |
-
|
|
|
|
|
|
|
|
|
|
| 1513 |
try:
|
| 1514 |
model.load_state_dict(_ckpt["model_state_dict"])
|
| 1515 |
_tokens_seen = _ckpt.get("tokens_seen")
|
|
@@ -1533,9 +1644,11 @@ def main_worker(rank: int, world_size: int, bin_path: str, total_tokens: int) ->
|
|
| 1533 |
resumed_step = _old_step // GRAD_ACCUM_STEPS
|
| 1534 |
resumed_step_is_approximate = _old_step > 0
|
| 1535 |
if accelerator.is_main_process:
|
| 1536 |
-
|
|
|
|
|
|
|
| 1537 |
print(f" ↳ poids chargés (step réel estimé: {resumed_step}, "
|
| 1538 |
-
f"val_loss précédent: {
|
| 1539 |
if resumed_step_is_approximate:
|
| 1540 |
print(f" ↳ ⚠️ Checkpoint antérieur à l'ajout de l'accumulation de gradient — "
|
| 1541 |
f"step converti par approximation (÷{GRAD_ACCUM_STEPS}), pas une valeur exacte.")
|
|
@@ -1548,7 +1661,7 @@ def main_worker(rank: int, world_size: int, bin_path: str, total_tokens: int) ->
|
|
| 1548 |
# repart de poids aléatoires — avec un avertissement explicite,
|
| 1549 |
# pour que ça ne passe jamais inaperçu.
|
| 1550 |
if accelerator.is_main_process:
|
| 1551 |
-
print(f"⚠️ '{
|
| 1552 |
f"(dim={args.dim}, n_layers={args.n_layers}) — probablement un checkpoint d'une "
|
| 1553 |
f"AUTRE taille de modèle. Poids ALÉATOIRES utilisés pour ce run (le fichier "
|
| 1554 |
f"n'est pas touché). Renomme/déplace-le si ce n'est pas voulu.\n Détail: {e}")
|
|
@@ -1602,7 +1715,12 @@ def main_worker(rank: int, world_size: int, bin_path: str, total_tokens: int) ->
|
|
| 1602 |
|
| 1603 |
data_iter = infinite_loader(loader)
|
| 1604 |
|
| 1605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1606 |
_last_latest_backup = time.time()
|
| 1607 |
# ⚠️ Throttle RETIRÉ : il causait un problème pire que celui qu'il évitait.
|
| 1608 |
# En repo PRIVÉ, on avait dépassé le quota de stockage (403) à force de
|
|
@@ -1683,6 +1801,14 @@ def main_worker(rank: int, world_size: int, bin_path: str, total_tokens: int) ->
|
|
| 1683 |
if accelerator.is_main_process:
|
| 1684 |
print("\n✅ Entraînement terminé.")
|
| 1685 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1686 |
if os.path.exists(CHECKPOINT_PATH):
|
| 1687 |
checkpoint = torch.load(CHECKPOINT_PATH, map_location=device, weights_only=False)
|
| 1688 |
raw_model.load_state_dict(checkpoint["model_state_dict"])
|
|
|
|
| 240 |
# "meilleur") existait juste avant le crash.
|
| 241 |
HF_BACKUP_LATEST_INTERVAL_SECONDS = 600 # 10 min
|
| 242 |
|
| 243 |
+
# --- Sauvegarde du CORPUS tokenisé (gros .bin, plusieurs heures de construction) ---
|
| 244 |
+
# Poussé UNE fois vers un repo HF de type "dataset" (distinct du repo de modèles),
|
| 245 |
+
# puis re-téléchargé automatiquement si le .bin local est absent ou vide
|
| 246 |
+
# (session Colab/Kaggle neuve, Drive corrompu...). Public par défaut: ce sont des
|
| 247 |
+
# données web publiques (Wikipedia / FineWeb-2) et ça évite les soucis de quota
|
| 248 |
+
# du stockage privé déjà rencontrés (403). Mets True pour un repo privé.
|
| 249 |
+
HF_CORPUS_BACKUP_ENABLED = True
|
| 250 |
+
HF_CORPUS_REPO_ID = "JuloEco/opsiom-fr-corpus" # ⚠️ adapte à ton propre namespace HF
|
| 251 |
+
HF_CORPUS_PRIVATE = False
|
| 252 |
+
|
| 253 |
+
_hf_apis = {}
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def _get_hf_api(repo_id: str, repo_type: str, private: bool):
|
| 257 |
+
"""Instancie paresseusement un client HfApi et crée le repo cible s'il
|
| 258 |
+
n'existe pas encore (un client mis en cache par couple repo_id/repo_type)."""
|
| 259 |
+
key = (repo_id, repo_type)
|
| 260 |
+
if key in _hf_apis:
|
| 261 |
+
return _hf_apis[key]
|
| 262 |
from huggingface_hub import HfApi
|
| 263 |
api = HfApi()
|
| 264 |
try:
|
| 265 |
+
api.create_repo(repo_id=repo_id, private=private, repo_type=repo_type, exist_ok=True)
|
| 266 |
except Exception as e:
|
| 267 |
+
print(f"⚠️ Impossible de créer/vérifier le repo HF '{repo_id}' ({repo_type}) ({e}). "
|
| 268 |
+
f"La sauvegarde externe correspondante est désactivée pour cette session.")
|
| 269 |
return None
|
| 270 |
+
_hf_apis[key] = api
|
| 271 |
return api
|
| 272 |
|
| 273 |
|
| 274 |
+
def push_backup_to_hf(local_path: str, path_in_repo: str, repo_id: str | None = None,
|
| 275 |
+
repo_type: str = "model", private: bool | None = None) -> None:
|
| 276 |
+
"""Pousse un fichier vers un repo HF Hub (par défaut: le repo de checkpoints).
|
| 277 |
+
Ne lève JAMAIS d'exception: un problème réseau ponctuel ne doit pas
|
| 278 |
+
interrompre l'entraînement — on log juste un avertissement et on continue."""
|
| 279 |
if not HF_BACKUP_ENABLED or not os.path.exists(local_path):
|
| 280 |
return
|
| 281 |
+
repo_id = repo_id or HF_BACKUP_REPO_ID
|
| 282 |
+
private = HF_BACKUP_PRIVATE if private is None else private
|
| 283 |
+
api = _get_hf_api(repo_id, repo_type, private)
|
| 284 |
if api is None:
|
| 285 |
return
|
| 286 |
try:
|
| 287 |
api.upload_file(
|
| 288 |
path_or_fileobj=local_path,
|
| 289 |
path_in_repo=path_in_repo,
|
| 290 |
+
repo_id=repo_id,
|
| 291 |
+
repo_type=repo_type,
|
| 292 |
)
|
| 293 |
size_mb = os.path.getsize(local_path) / 1e6
|
| 294 |
+
print(f" ↳ 🛟 Sauvegarde externe: '{path_in_repo}' poussé vers {repo_id} ({size_mb:.1f} Mo)")
|
| 295 |
except Exception as e:
|
| 296 |
print(f" ↳ ⚠️ Échec de la sauvegarde externe de '{path_in_repo}' ({e}) — entraînement non interrompu.")
|
| 297 |
|
| 298 |
|
| 299 |
+
def try_restore_backup_from_hf(local_path: str, path_in_repo: str, repo_id: str | None = None,
|
| 300 |
+
repo_type: str = "model", direct: bool = False) -> bool:
|
| 301 |
+
"""Tente de restaurer un fichier depuis un repo HF Hub si le fichier local
|
| 302 |
+
est absent — utile après une session perdue, pour reprendre automatiquement
|
| 303 |
+
sans réimport manuel. `direct=True` télécharge directement dans le dossier
|
| 304 |
+
de destination (pas de copie via le cache HF): indispensable pour un fichier
|
| 305 |
+
de plusieurs Go. Retourne True si un fichier a été restauré."""
|
| 306 |
if not HF_BACKUP_ENABLED or os.path.exists(local_path):
|
| 307 |
return False
|
| 308 |
+
repo_id = repo_id or HF_BACKUP_REPO_ID
|
| 309 |
try:
|
| 310 |
from huggingface_hub import hf_hub_download
|
|
|
|
|
|
|
| 311 |
os.makedirs(os.path.dirname(local_path) or ".", exist_ok=True)
|
| 312 |
+
if direct:
|
| 313 |
+
target_dir = os.path.dirname(local_path) or "."
|
| 314 |
+
downloaded = hf_hub_download(repo_id=repo_id, filename=path_in_repo,
|
| 315 |
+
repo_type=repo_type, local_dir=target_dir)
|
| 316 |
+
if os.path.abspath(downloaded) != os.path.abspath(local_path):
|
| 317 |
+
os.replace(downloaded, local_path)
|
| 318 |
+
else:
|
| 319 |
+
downloaded = hf_hub_download(repo_id=repo_id, filename=path_in_repo, repo_type=repo_type)
|
| 320 |
+
import shutil
|
| 321 |
+
shutil.copy2(downloaded, local_path)
|
| 322 |
+
print(f"♻️ Restauré depuis HF Hub ({repo_id}): '{path_in_repo}' -> '{local_path}'")
|
| 323 |
return True
|
| 324 |
except Exception as e:
|
| 325 |
+
print(f"ℹ️ Pas de sauvegarde externe utilisable pour '{path_in_repo}' sur {repo_id} ({e}).")
|
| 326 |
return False
|
| 327 |
|
| 328 |
|
| 329 |
+
def restore_corpus_from_hf(bin_path: str, meta_path: str) -> bool:
|
| 330 |
+
"""Si le corpus .bin est absent ou vide en local, le re-télécharge depuis le
|
| 331 |
+
repo HF 'dataset' (avec son meta). Ne touche à rien si un .bin non vide existe."""
|
| 332 |
+
if not (HF_BACKUP_ENABLED and HF_CORPUS_BACKUP_ENABLED):
|
| 333 |
+
return False
|
| 334 |
+
if os.path.exists(bin_path) and os.path.getsize(bin_path) > 0:
|
| 335 |
+
return False
|
| 336 |
+
if os.path.exists(bin_path):
|
| 337 |
+
os.remove(bin_path) # fichier vide: sinon try_restore le considérerait "présent"
|
| 338 |
+
print(f"⬇️ Corpus local absent/vide — tentative de téléchargement depuis {HF_CORPUS_REPO_ID} "
|
| 339 |
+
f"(plusieurs Go, sans barre de progression, patiente)...")
|
| 340 |
+
ok = try_restore_backup_from_hf(bin_path, os.path.basename(bin_path),
|
| 341 |
+
repo_id=HF_CORPUS_REPO_ID, repo_type="dataset", direct=True)
|
| 342 |
+
if not ok:
|
| 343 |
+
return False
|
| 344 |
+
old_meta = None
|
| 345 |
+
if os.path.exists(meta_path):
|
| 346 |
+
with open(meta_path, "rb") as f:
|
| 347 |
+
old_meta = f.read()
|
| 348 |
+
os.remove(meta_path)
|
| 349 |
+
if not try_restore_backup_from_hf(meta_path, os.path.basename(meta_path),
|
| 350 |
+
repo_id=HF_CORPUS_REPO_ID, repo_type="dataset", direct=True):
|
| 351 |
+
if old_meta is not None:
|
| 352 |
+
with open(meta_path, "wb") as f:
|
| 353 |
+
f.write(old_meta)
|
| 354 |
+
return True
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def push_corpus_to_hf_if_needed(bin_path: str, meta_path: str) -> None:
|
| 358 |
+
"""Pousse le corpus (.bin puis .meta) vers le repo HF 'dataset'. Le .bin
|
| 359 |
+
(~3,5 Go) n'est renvoyé que si la taille distante diffère de la taille
|
| 360 |
+
locale: pas de ré-upload inutile à chaque lancement. Ne lève jamais."""
|
| 361 |
+
if not (HF_BACKUP_ENABLED and HF_CORPUS_BACKUP_ENABLED):
|
| 362 |
+
return
|
| 363 |
+
if not (os.path.exists(bin_path) and os.path.getsize(bin_path) > 0 and os.path.exists(meta_path)):
|
| 364 |
+
return
|
| 365 |
+
api = _get_hf_api(HF_CORPUS_REPO_ID, "dataset", HF_CORPUS_PRIVATE)
|
| 366 |
+
if api is None:
|
| 367 |
+
return
|
| 368 |
+
bin_name = os.path.basename(bin_path)
|
| 369 |
+
try:
|
| 370 |
+
infos = api.get_paths_info(HF_CORPUS_REPO_ID, [bin_name], repo_type="dataset")
|
| 371 |
+
if infos and infos[0].size == os.path.getsize(bin_path):
|
| 372 |
+
print(f"✅ Corpus déjà sauvegardé sur {HF_CORPUS_REPO_ID} (même taille) — pas de ré-envoi.")
|
| 373 |
+
else:
|
| 374 |
+
raise FileNotFoundError("absent ou taille différente")
|
| 375 |
+
except Exception:
|
| 376 |
+
print(f"⬆️ Envoi du corpus ({os.path.getsize(bin_path) / 1e9:.2f} Go) vers {HF_CORPUS_REPO_ID} — "
|
| 377 |
+
f"quelques minutes la première fois...")
|
| 378 |
+
push_backup_to_hf(bin_path, bin_name, repo_id=HF_CORPUS_REPO_ID,
|
| 379 |
+
repo_type="dataset", private=HF_CORPUS_PRIVATE)
|
| 380 |
+
# Le meta (quelques centaines d'octets) est toujours repoussé, après le .bin.
|
| 381 |
+
push_backup_to_hf(meta_path, os.path.basename(meta_path), repo_id=HF_CORPUS_REPO_ID,
|
| 382 |
+
repo_type="dataset", private=HF_CORPUS_PRIVATE)
|
| 383 |
+
|
| 384 |
+
|
| 385 |
# ============================================================================
|
| 386 |
# Configuration
|
| 387 |
# ============================================================================
|
|
|
|
| 441 |
GEN_INTERVAL = 400 # Génération d'un échantillon de contrôle tous les N steps
|
| 442 |
CHECKPOINT_PATH = os.path.join(_SAVE_DIR, "best_model_medium.pt")
|
| 443 |
LATEST_CHECKPOINT_PATH = os.path.join(_SAVE_DIR, "latest_model_medium.pt")
|
| 444 |
+
FINAL_CHECKPOINT_PATH = os.path.join(_SAVE_DIR, "final_model_medium.pt")
|
| 445 |
SEED = 1337
|
| 446 |
|
| 447 |
# Cible de tokens pour le pré-entraînement à grande échelle (modèle "Large").
|
|
|
|
| 1261 |
os.remove(bin_path)
|
| 1262 |
os.replace(alt_path, bin_path)
|
| 1263 |
|
| 1264 |
+
# ⬇️ Dernier recours avant une reconstruction de plusieurs heures: si le .bin
|
| 1265 |
+
# est toujours absent/vide, on tente de le re-télécharger depuis HF Hub.
|
| 1266 |
+
restore_corpus_from_hf(bin_path, meta_path)
|
| 1267 |
+
|
| 1268 |
if os.path.exists(bin_path) and os.path.exists(meta_path):
|
| 1269 |
with open(meta_path, "r", encoding="utf-8") as f:
|
| 1270 |
meta = json.load(f)
|
|
|
|
| 1522 |
bin_path, total_tokens = build_pretraining_corpus_bin(
|
| 1523 |
tokenizer, target_tokens=TARGET_PRETRAIN_TOKENS, extra_text_once=seed_text,
|
| 1524 |
)
|
| 1525 |
+
# 🛟 Sauvegarde du corpus sur HF Hub (no-op si déjà présent avec la même taille).
|
| 1526 |
+
push_corpus_to_hf_if_needed(bin_path, PRETRAIN_META_PATH)
|
| 1527 |
return bin_path, total_tokens
|
| 1528 |
|
| 1529 |
|
| 1530 |
+
def _checkpoint_progress(path: str) -> int:
|
| 1531 |
+
"""Nombre de tokens vus enregistré dans un checkpoint (0 si absent/illisible/
|
| 1532 |
+
ancien format sans 'tokens_seen')."""
|
| 1533 |
+
try:
|
| 1534 |
+
c = torch.load(path, map_location="cpu", weights_only=False)
|
| 1535 |
+
return int(c.get("tokens_seen") or 0)
|
| 1536 |
+
except Exception:
|
| 1537 |
+
return -1
|
| 1538 |
+
|
| 1539 |
+
|
| 1540 |
+
def _pick_resume_checkpoint(paths: list[str]) -> str | None:
|
| 1541 |
+
"""Parmi les checkpoints locaux existants, renvoie celui qui a vu le plus de
|
| 1542 |
+
tokens (en cas d'égalité, le premier de la liste, donc 'best')."""
|
| 1543 |
+
existing = [p for p in paths if os.path.exists(p)]
|
| 1544 |
+
if not existing:
|
| 1545 |
+
return None
|
| 1546 |
+
return max(existing, key=lambda p: (_checkpoint_progress(p), -existing.index(p)))
|
| 1547 |
+
|
| 1548 |
+
|
| 1549 |
def main_worker(rank: int, world_size: int, bin_path: str, total_tokens: int) -> None:
|
| 1550 |
|
| 1551 |
if world_size > 1:
|
|
|
|
| 1606 |
# depuis la sauvegarde externe HF Hub (utile après une session Kaggle
|
| 1607 |
# perdue sans 'Save Version'), PUIS le comportement RESUME_FROM_CHECKPOINT
|
| 1608 |
# habituel sur le fichier local (potentiellement fraîchement restauré).
|
| 1609 |
+
if accelerator.is_main_process and RESUME_FROM_CHECKPOINT:
|
| 1610 |
+
if not os.path.exists(CHECKPOINT_PATH):
|
| 1611 |
+
try_restore_backup_from_hf(CHECKPOINT_PATH, "best_model_medium.pt")
|
| 1612 |
+
if not os.path.exists(LATEST_CHECKPOINT_PATH):
|
| 1613 |
+
try_restore_backup_from_hf(LATEST_CHECKPOINT_PATH, "latest_model_medium.pt")
|
| 1614 |
if accelerator.num_processes > 1:
|
| 1615 |
accelerator.wait_for_everyone() # tous les rangs attendent la restauration éventuelle du rang 0
|
| 1616 |
|
| 1617 |
resumed_step = 0
|
| 1618 |
resumed_step_is_approximate = False
|
| 1619 |
+
# On reprend du checkpoint le plus avancé entre "best" et "latest" (le latest
|
| 1620 |
+
# peut être plus récent que le best s'il n'y a pas eu d'amélioration de val_loss).
|
| 1621 |
+
_resume_path = _pick_resume_checkpoint([CHECKPOINT_PATH, LATEST_CHECKPOINT_PATH]) if RESUME_FROM_CHECKPOINT else None
|
| 1622 |
+
if _resume_path is not None:
|
| 1623 |
+
_ckpt = torch.load(_resume_path, map_location="cpu", weights_only=False)
|
| 1624 |
try:
|
| 1625 |
model.load_state_dict(_ckpt["model_state_dict"])
|
| 1626 |
_tokens_seen = _ckpt.get("tokens_seen")
|
|
|
|
| 1644 |
resumed_step = _old_step // GRAD_ACCUM_STEPS
|
| 1645 |
resumed_step_is_approximate = _old_step > 0
|
| 1646 |
if accelerator.is_main_process:
|
| 1647 |
+
_pv = _ckpt.get("val_loss")
|
| 1648 |
+
_pv_txt = f"{_pv:.4f}" if _pv is not None else "n/a (checkpoint 'latest')"
|
| 1649 |
+
print(f"♻️ Reprise depuis '{_resume_path}' (au lieu de repartir de poids aléatoires)...")
|
| 1650 |
print(f" ↳ poids chargés (step réel estimé: {resumed_step}, "
|
| 1651 |
+
f"val_loss précédent: {_pv_txt})")
|
| 1652 |
if resumed_step_is_approximate:
|
| 1653 |
print(f" ↳ ⚠️ Checkpoint antérieur à l'ajout de l'accumulation de gradient — "
|
| 1654 |
f"step converti par approximation (÷{GRAD_ACCUM_STEPS}), pas une valeur exacte.")
|
|
|
|
| 1661 |
# repart de poids aléatoires — avec un avertissement explicite,
|
| 1662 |
# pour que ça ne passe jamais inaperçu.
|
| 1663 |
if accelerator.is_main_process:
|
| 1664 |
+
print(f"⚠️ '{_resume_path}' existe mais ne correspond pas à l'architecture actuelle "
|
| 1665 |
f"(dim={args.dim}, n_layers={args.n_layers}) — probablement un checkpoint d'une "
|
| 1666 |
f"AUTRE taille de modèle. Poids ALÉATOIRES utilisés pour ce run (le fichier "
|
| 1667 |
f"n'est pas touché). Renomme/déplace-le si ce n'est pas voulu.\n Détail: {e}")
|
|
|
|
| 1715 |
|
| 1716 |
data_iter = infinite_loader(loader)
|
| 1717 |
|
| 1718 |
+
# Meilleur val_loss connu: si on reprend depuis "latest" (val_loss=None), on le
|
| 1719 |
+
# relit dans le fichier "best" pour ne PAS écraser un meilleur modèle par un pire.
|
| 1720 |
+
_prev_best = _ckpt.get("val_loss") if resumed_step else None
|
| 1721 |
+
if resumed_step and _prev_best is None and os.path.exists(CHECKPOINT_PATH):
|
| 1722 |
+
_prev_best = torch.load(CHECKPOINT_PATH, map_location="cpu", weights_only=False).get("val_loss")
|
| 1723 |
+
best_val_loss = float(_prev_best) if _prev_best is not None else float("inf")
|
| 1724 |
_last_latest_backup = time.time()
|
| 1725 |
# ⚠️ Throttle RETIRÉ : il causait un problème pire que celui qu'il évitait.
|
| 1726 |
# En repo PRIVÉ, on avait dépassé le quota de stockage (403) à force de
|
|
|
|
| 1801 |
if accelerator.is_main_process:
|
| 1802 |
print("\n✅ Entraînement terminé.")
|
| 1803 |
|
| 1804 |
+
# 🛟 État final (dernier step, pas forcément le "best") sauvegardé et poussé.
|
| 1805 |
+
torch.save(
|
| 1806 |
+
{"model_state_dict": raw_model.state_dict(), "args": args, "step": effective_max_steps - 1,
|
| 1807 |
+
"val_loss": None, "tokens_seen": effective_max_steps * tokens_per_micro_step * GRAD_ACCUM_STEPS},
|
| 1808 |
+
FINAL_CHECKPOINT_PATH,
|
| 1809 |
+
)
|
| 1810 |
+
push_backup_to_hf(FINAL_CHECKPOINT_PATH, "final_model_medium.pt")
|
| 1811 |
+
|
| 1812 |
if os.path.exists(CHECKPOINT_PATH):
|
| 1813 |
checkpoint = torch.load(CHECKPOINT_PATH, map_location=device, weights_only=False)
|
| 1814 |
raw_model.load_state_dict(checkpoint["model_state_dict"])
|