JuloEco commited on
Commit
28531dd
·
verified ·
1 Parent(s): e0723c3

Update main_medium.py

Browse files
Files changed (1) hide show
  1. 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
- _hf_backup_api = None
244
-
245
-
246
- def _get_hf_backup_api():
247
- """Instancie paresseusement le client HfApi (une seule fois), et
248
- crée le repo de sauvegarde s'il n'existe pas déjà."""
249
- global _hf_backup_api
250
- if _hf_backup_api is not None:
251
- return _hf_backup_api
 
 
 
 
 
 
 
 
 
 
252
  from huggingface_hub import HfApi
253
  api = HfApi()
254
  try:
255
- api.create_repo(repo_id=HF_BACKUP_REPO_ID, private=HF_BACKUP_PRIVATE, repo_type="model", exist_ok=True)
256
  except Exception as e:
257
- print(f"⚠️ Impossible de créer/vérifier le repo de sauvegarde HF '{HF_BACKUP_REPO_ID}' ({e}). "
258
- f"La sauvegarde externe est désactivée pour cette session.")
259
  return None
260
- _hf_backup_api = api
261
  return api
262
 
263
 
264
- def push_backup_to_hf(local_path: str, path_in_repo: str) -> None:
265
- """Pousse un fichier vers le repo de sauvegarde HF Hub. Ne lève JAMAIS
266
- d'exception: un problème réseau ponctuel ne doit pas interrompre
267
- l'entraînement — on log juste un avertissement et on continue."""
 
268
  if not HF_BACKUP_ENABLED or not os.path.exists(local_path):
269
  return
270
- api = _get_hf_backup_api()
 
 
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=HF_BACKUP_REPO_ID,
278
- repo_type="model",
279
  )
280
  size_mb = os.path.getsize(local_path) / 1e6
281
- print(f" ↳ 🛟 Sauvegarde externe: '{path_in_repo}' poussé vers {HF_BACKUP_REPO_ID} ({size_mb:.1f} Mo)")
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) -> bool:
287
- """Tente de restaurer un fichier depuis le repo de sauvegarde HF Hub si le
288
- fichier local est absent — utile après une session Kaggle perdue sans
289
- 'Save Version', pour reprendre automatiquement sans réimport manuel.
290
- Retourne True si un fichier a été restauré."""
 
 
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
- shutil.copy2(downloaded, local_path)
299
- print(f"♻️ Restauré depuis la sauvegarde externe HF Hub: '{path_in_repo}' -> '{local_path}'")
 
 
 
 
 
 
 
 
 
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 and not os.path.exists(CHECKPOINT_PATH):
1505
- try_restore_backup_from_hf(CHECKPOINT_PATH, "best_model_medium.pt")
 
 
 
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
- if RESUME_FROM_CHECKPOINT and os.path.exists(CHECKPOINT_PATH):
1512
- _ckpt = torch.load(CHECKPOINT_PATH, map_location="cpu", weights_only=False)
 
 
 
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
- print(f"♻️ Reprise depuis '{CHECKPOINT_PATH}' (au lieu de repartir de poids aléatoires)...")
 
 
1537
  print(f" ↳ poids chargés (step réel estimé: {resumed_step}, "
1538
- f"val_loss précédent: {_ckpt.get('val_loss', float('nan')):.4f})")
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"⚠️ '{CHECKPOINT_PATH}' existe mais ne correspond pas à l'architecture actuelle "
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
- best_val_loss = float(_ckpt.get("val_loss", float("inf"))) if resumed_step else float("inf")
 
 
 
 
 
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"])