functionNormally commited on
Commit
72ef639
·
1 Parent(s): c72538b

Calibrer les durees ZeroGPU sur des mesures reelles et exposer le telechargement des modeles

Browse files

Le duration= des decorateurs @spaces.GPU etait un plafond fixe (300s) quel
que soit l'entrainement demande, alors que ZeroGPU reserve la duree
declaree (pas le temps reellement mesure) sur le quota journalier de la
personne qui clique, pas sur celui du proprietaire du Space. Pour un
compte gratuit (300s/jour), un seul entrainement MLP ou CNN epuisait donc
tout le quota du jour.

MLP et CNN utilisent maintenant une duree dynamique calibree sur des
mesures reelles (66.6s / 140.8s pour le MLP, 65.3s / 153.9s pour le CNN
selon les hyperparametres), plafonnee a 180s pour rester nettement sous
le quota journalier gratuit. Les curseurs d'architecture les plus extremes
(hidden_dim=1024, 5 couches/blocs, filtres=128) sont retires de l'UI :
les mesures montrent que le nombre de parametres surestime tres largement
le cout reel sur ce jeu de donnees minuscule, dont le temps est domine par
un overhead fixe plutot que par les FLOPs du modele.

Ajout d'un composant de telechargement (poids .pt ou pipeline .joblib)
pour le modele selectionne dans l'onglet 5, absent jusqu'ici alors que
c'etait la seule facon de recuperer un modele entraine hors du Space.

Files changed (2) hide show
  1. app.py +65 -6
  2. train_utils.py +11 -0
app.py CHANGED
@@ -11,6 +11,7 @@ from train_utils import (
11
  evaluate_saved_model,
12
  list_saved_models,
13
  model_meta_path,
 
14
  train_cnn,
15
  train_mlp,
16
  )
@@ -39,7 +40,27 @@ def refresh_gallery_callback(split_name, class_name, max_images):
39
  # Tab 2 — MLP (baseline)
40
  # ---------------------------------------------------------------------------
41
 
42
- @spaces.GPU(duration=300)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def train_mlp_callback(
44
  num_layers, hidden_dim, dropout,
45
  learning_rate, weight_decay, batch_size, epochs,
@@ -79,7 +100,31 @@ def train_mlp_callback(
79
  # Tab 3 — SimpleCNN
80
  # ---------------------------------------------------------------------------
81
 
82
- @spaces.GPU(duration=300)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def train_cnn_callback(
84
  num_conv_blocks, base_filters, kernel_size, use_batchnorm,
85
  dropout, fc_dim,
@@ -207,6 +252,15 @@ def get_model_info_callback(model_name, request: gr.Request):
207
  return {"message": "Métadonnées introuvables."}
208
 
209
 
 
 
 
 
 
 
 
 
 
210
  @spaces.GPU(duration=120)
211
  def evaluate_callback(model_name, request: gr.Request):
212
  try:
@@ -309,11 +363,11 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
309
  with gr.Column():
310
  gr.Markdown("#### Architecture du MLP")
311
  mlp_num_layers = gr.Slider(
312
- minimum=1, maximum=5, value=2, step=1,
313
  label="Nombre de couches cachées",
314
  )
315
  mlp_hidden_dim = gr.Dropdown(
316
- choices=[64, 128, 256, 512, 1024], value=256,
317
  label="Neurones par couche cachée",
318
  )
319
 
@@ -370,12 +424,12 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
370
  with gr.Column():
371
  gr.Markdown("#### Architecture du CNN")
372
  num_conv_blocks = gr.Slider(
373
- minimum=2, maximum=5, value=3, step=1,
374
  label="Blocs convolutionnels",
375
  info="Chaque bloc enchaîne Conv2d → BatchNorm → ReLU → MaxPool. Plus de blocs = réseau plus profond.",
376
  )
377
  base_filters = gr.Dropdown(
378
- choices=[16, 32, 64, 128], value=32,
379
  label="Filtres du premier bloc",
380
  info="Le nombre de filtres double à chaque bloc. 32 → 64 → 128...",
381
  )
@@ -546,6 +600,7 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
546
  refresh_btn = gr.Button("Actualiser la liste")
547
  load_info_btn = gr.Button("Afficher les informations du modèle")
548
  model_info = gr.JSON(label="Métadonnées du modèle")
 
549
 
550
  with gr.Column():
551
  evaluate_btn = gr.Button("Évaluer sur le jeu de test", variant="primary")
@@ -652,6 +707,10 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
652
  fn=get_model_info_callback, inputs=model_selector, outputs=model_info
653
  )
654
 
 
 
 
 
655
  evaluate_btn.click(
656
  fn=evaluate_callback,
657
  inputs=model_selector,
 
11
  evaluate_saved_model,
12
  list_saved_models,
13
  model_meta_path,
14
+ saved_model_file_path,
15
  train_cnn,
16
  train_mlp,
17
  )
 
40
  # Tab 2 — MLP (baseline)
41
  # ---------------------------------------------------------------------------
42
 
43
+ def mlp_gpu_duration(
44
+ num_layers, hidden_dim, dropout,
45
+ learning_rate, weight_decay, batch_size, epochs,
46
+ model_tag,
47
+ request: gr.Request,
48
+ ):
49
+ # Calibré sur deux exécutions réelles :
50
+ # 2 couches, hidden_dim=256, epochs=30 -> 66.6s (2.22 s/époque)
51
+ # 5 couches, hidden_dim=1024, epochs=50 -> 140.8s (2.82 s/époque)
52
+ # Le jeu de données est minuscule (peu de pas par époque) : le temps est
53
+ # dominé par un overhead fixe, hidden_dim ne le fait varier que doucement
54
+ # (x4 sur hidden_dim -> seulement +27% par époque). On plafonne à 180s :
55
+ # nettement sous le quota journalier d'un compte gratuit (300s), pour
56
+ # qu'un seul entraînement au pire réglage ne consomme pas tout le quota
57
+ # du jour d'un·e étudiant·e.
58
+ per_epoch = 2.22 + 0.0008 * max(0, int(hidden_dim) - 256)
59
+ estimated = 15 + per_epoch * int(epochs)
60
+ return min(180, max(45, int(estimated * 1.4)))
61
+
62
+
63
+ @spaces.GPU(duration=mlp_gpu_duration)
64
  def train_mlp_callback(
65
  num_layers, hidden_dim, dropout,
66
  learning_rate, weight_decay, batch_size, epochs,
 
100
  # Tab 3 — SimpleCNN
101
  # ---------------------------------------------------------------------------
102
 
103
+ def cnn_gpu_duration(
104
+ num_conv_blocks, base_filters, kernel_size, use_batchnorm,
105
+ dropout, fc_dim,
106
+ learning_rate, weight_decay, batch_size, epochs,
107
+ model_tag,
108
+ request: gr.Request,
109
+ ):
110
+ # Calibré sur deux exécutions réelles :
111
+ # 3 blocs, filtres=32, noyau=3, epochs=30 -> 65.3s (2.18 s/époque)
112
+ # 5 blocs, filtres=128, noyau=5, epochs=50 -> 153.9s (3.08 s/époque)
113
+ # Le nombre de paramètres varie de ~130x entre ces deux essais mais le temps
114
+ # par époque seulement de 40% : sur ce jeu de données minuscule, le coût est
115
+ # dominé par un overhead fixe (chargement/augmentation), pas par les FLOPs
116
+ # du réseau — le nombre de paramètres surestimerait donc très largement.
117
+ # On interpole plutôt sur un score d'architecture simple. Plafond 180s :
118
+ # nettement sous le quota journalier d'un compte gratuit (300s).
119
+ score = int(num_conv_blocks) * int(base_filters) * (int(kernel_size) / 3)
120
+ baseline_score, worst_score = 96.0, 1066.7
121
+ frac = max(0.0, min(1.0, (score - baseline_score) / (worst_score - baseline_score)))
122
+ per_epoch = 2.18 + 0.9 * frac
123
+ estimated = 15 + per_epoch * int(epochs)
124
+ return min(180, max(45, int(estimated * 1.4)))
125
+
126
+
127
+ @spaces.GPU(duration=cnn_gpu_duration)
128
  def train_cnn_callback(
129
  num_conv_blocks, base_filters, kernel_size, use_batchnorm,
130
  dropout, fc_dim,
 
252
  return {"message": "Métadonnées introuvables."}
253
 
254
 
255
+ def download_model_callback(model_name, request: gr.Request):
256
+ if not model_name:
257
+ return gr.update(value=None, visible=False)
258
+ try:
259
+ return gr.update(value=saved_model_file_path(model_name, request.session_hash), visible=True)
260
+ except FileNotFoundError:
261
+ return gr.update(value=None, visible=False)
262
+
263
+
264
  @spaces.GPU(duration=120)
265
  def evaluate_callback(model_name, request: gr.Request):
266
  try:
 
363
  with gr.Column():
364
  gr.Markdown("#### Architecture du MLP")
365
  mlp_num_layers = gr.Slider(
366
+ minimum=1, maximum=4, value=2, step=1,
367
  label="Nombre de couches cachées",
368
  )
369
  mlp_hidden_dim = gr.Dropdown(
370
+ choices=[64, 128, 256, 512], value=256,
371
  label="Neurones par couche cachée",
372
  )
373
 
 
424
  with gr.Column():
425
  gr.Markdown("#### Architecture du CNN")
426
  num_conv_blocks = gr.Slider(
427
+ minimum=2, maximum=4, value=3, step=1,
428
  label="Blocs convolutionnels",
429
  info="Chaque bloc enchaîne Conv2d → BatchNorm → ReLU → MaxPool. Plus de blocs = réseau plus profond.",
430
  )
431
  base_filters = gr.Dropdown(
432
+ choices=[16, 32, 64], value=32,
433
  label="Filtres du premier bloc",
434
  info="Le nombre de filtres double à chaque bloc. 32 → 64 → 128...",
435
  )
 
600
  refresh_btn = gr.Button("Actualiser la liste")
601
  load_info_btn = gr.Button("Afficher les informations du modèle")
602
  model_info = gr.JSON(label="Métadonnées du modèle")
603
+ model_download = gr.File(label="Télécharger le modèle sélectionné", visible=False)
604
 
605
  with gr.Column():
606
  evaluate_btn = gr.Button("Évaluer sur le jeu de test", variant="primary")
 
707
  fn=get_model_info_callback, inputs=model_selector, outputs=model_info
708
  )
709
 
710
+ model_selector.change(
711
+ fn=download_model_callback, inputs=model_selector, outputs=model_download
712
+ )
713
+
714
  evaluate_btn.click(
715
  fn=evaluate_callback,
716
  inputs=model_selector,
train_utils.py CHANGED
@@ -8,6 +8,7 @@ import torch
8
  import torch.nn as nn
9
  import torch.optim as optim
10
 
 
11
  from config import DATASET_DISPLAY_NAME, CLASSICAL_MODEL_TYPES, IMAGE_SIZE, session_model_dir, session_meta_dir
12
  from data_utils import make_loaders
13
  from metrics_utils import compute_classification_metrics, save_confusion_matrix_figure, save_loss_curve_figure
@@ -41,6 +42,16 @@ def get_runtime_device() -> torch.device:
41
  return torch.device("cuda" if torch.cuda.is_available() else "cpu")
42
 
43
 
 
 
 
 
 
 
 
 
 
 
44
  # ---------------------------------------------------------------------------
45
  # Save / load
46
  # ---------------------------------------------------------------------------
 
8
  import torch.nn as nn
9
  import torch.optim as optim
10
 
11
+ from classical_ml_utils import classifier_path
12
  from config import DATASET_DISPLAY_NAME, CLASSICAL_MODEL_TYPES, IMAGE_SIZE, session_model_dir, session_meta_dir
13
  from data_utils import make_loaders
14
  from metrics_utils import compute_classification_metrics, save_confusion_matrix_figure, save_loss_curve_figure
 
42
  return torch.device("cuda" if torch.cuda.is_available() else "cpu")
43
 
44
 
45
+ def saved_model_file_path(model_name: str, session_id: str) -> str:
46
+ """Chemin du fichier de poids téléchargeable — .joblib pour les
47
+ classifieurs ML classiques, .pt pour les modèles neuronaux."""
48
+ with open(model_meta_path(model_name, session_id), "r", encoding="utf-8") as f:
49
+ model_type = json.load(f)["config"]["model_type"]
50
+ if model_type in CLASSICAL_MODEL_TYPES:
51
+ return classifier_path(model_name, session_id)
52
+ return model_weight_path(model_name, session_id)
53
+
54
+
55
  # ---------------------------------------------------------------------------
56
  # Save / load
57
  # ---------------------------------------------------------------------------