diff --git a/docs/sessions/03-pipeline-assets.md b/docs/sessions/03-pipeline-assets.md index c9428d0..6f4fa2f 100644 --- a/docs/sessions/03-pipeline-assets.md +++ b/docs/sessions/03-pipeline-assets.md @@ -60,16 +60,35 @@ serait mettre la charrue avant les bœufs. Le porteur a ouvert l'accès à son instance (`127.0.0.1:8188`, ComfyUI 0.27.1, deux GPU AMD). Le pipeline a donc été exécuté pour de vrai, pas simulé. -### Reproductibilité prouvée au bit près +### Reproductibilité prouvée — après correction d'un test qui ne prouvait rien -| Exécution | seed | SHA-256 de l'image | -|---|---|---| -| 1 | 3475989105 | `b7007a7a1088cb359a06ddaaff358e92…` | -| 2 | 3475989105 | `b7007a7a1088cb359a06ddaaff358e92…` | +**Images bit-identiques**, `sha256 b7007a7a1088cb359a06ddaaff358e92…`, sur deux exécutions +séparées par un **vidage du cache**. Workflow de référence : `workflows/decor-sdxl.json` +(SDXL 1.0, 1024×576, 20 pas, dpmpp_2m/karras). -**Images bit-identiques.** C'est précisément ce que le pack d'origine annonçait sans y -parvenir, et c'est maintenant vérifié plutôt qu'affirmé. Workflow de référence : -`workflows/decor-sdxl.json` (SDXL 1.0, 1024×576, 20 pas, dpmpp_2m/karras). +Le mot « après vidage du cache » est tout le sujet, et il a failli manquer. + +Ma première vérification lançait simplement le même plan deux fois et comparait les +empreintes. Le porteur ayant indiqué où ComfyUI écrit ses images +(`/media/sucupira/DKXONE/ComfyUI/output`), le dossier a pu être inspecté : **un seul +fichier y figurait** alors que deux exécutions avaient été lancées. + +ComfyUI met en cache le résultat d'un graphe identique. Ma « deuxième exécution » n'avait +donc rien recalculé — elle avait relu la première. Comparer deux empreintes dans ces +conditions ne mesure que le bon fonctionnement du cache. + +Après `POST /free`, une exécution authentique a produit `decor_00002_.png`, bit-identique +au premier. **La conclusion était juste ; la méthode ne l'était pas.** + +Le test est désormais un mode de l'outil, pas une manipulation à la main : + +```bash +python3 comfyui_client.py --workflow workflows/decor-sdxl.json \ + --plan plan-assets.json --reproductibilite +``` + +Il génère, vide le cache, régénère et compare. Un test de reproductibilité qui ne vide pas +le cache est un test qui se ment à lui-même. ### Deux défauts de mon propre client, révélés par le réel diff --git a/package.json b/package.json index dac32c4..eec259a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "fetch:medias": "node tools/data/fetch-medias.mjs", "verify:seeds": "python3 tools/assets/comfyui_client.py --verifier", "verify:candidats": "node tools/data/verify-candidats.mjs", - "audit": "node tools/build/audit.mjs" + "audit": "node tools/build/audit.mjs", + "verify:reproductibilite": "python3 tools/assets/comfyui_client.py --workflow tools/assets/workflows/decor-sdxl.json --plan tools/assets/plan-assets.json --reproductibilite" }, "devDependencies": { "@fontsource/archivo": "^5.3.0", diff --git a/tools/assets/README.md b/tools/assets/README.md index ffaa0eb..5dbabfe 100644 --- a/tools/assets/README.md +++ b/tools/assets/README.md @@ -15,12 +15,23 @@ Un enfant qui apprend dessus apprend l'hallucination d'un modèle. Le décor, lui, n'a rien à enseigner. C'est là que la génération a sa place. +## Où ComfyUI écrit + +Sur la machine du porteur : `/media/sucupira/DKXONE/ComfyUI/output`, sous-dossier `ki-fwi/` +(fixé par `filename_prefix` dans le workflow). Le client rapatrie les images par l'API +`/view`, ce qui fonctionne aussi si ComfyUI tourne ailleurs — mais inspecter ce dossier +reste le seul moyen de savoir si une génération a **réellement** eu lieu. + ## Utilisation ```bash # Vérifier que les seeds sont bien déterministes (aucun serveur requis) python3 comfyui_client.py --verifier +# Test de reproductibilité RÉEL : génère, vide le cache, régénère, compare +python3 comfyui_client.py --workflow workflows/decor-sdxl.json \ + --plan plan-assets.json --reproductibilite + # Calculer les seeds d'un plan sans rien générer python3 comfyui_client.py --workflow workflows/decor.json --plan plan-assets.json --sec @@ -80,3 +91,16 @@ c'est elle qui garantit qu'une régénération donne la même image. Il ne valide rien. Tout asset sort en `valide: false` et le reste jusqu'à ce qu'un humain tranche — comme pour les photographies. La différence est qu'un asset de filière B, même validé, ne pourra jamais porter une épreuve. + +## Piège du cache d'exécution + +**ComfyUI met en cache le résultat d'un graphe identique.** Relancer le même workflow ne +recalcule rien : aucune image n'est écrite, et l'API renvoie la sortie précédente. + +Conséquence : comparer deux « exécutions » sans vider le cache ne prouve rien sur la +reproductibilité — cela prouve seulement que le cache fonctionne. C'est l'erreur commise +puis corrigée en session 3. + +`--reproductibilite` appelle `POST /free` entre les deux générations. Pour vérifier à la +main, regarder si un nouveau fichier est apparu dans le dossier de sortie : s'il n'y en a +pas, rien n'a été calculé. diff --git a/tools/assets/comfyui_client.py b/tools/assets/comfyui_client.py index 67d2548..3c9d745 100644 --- a/tools/assets/comfyui_client.py +++ b/tools/assets/comfyui_client.py @@ -71,6 +71,29 @@ def empreinte(chemin: Path) -> str: return h.hexdigest() +def vider_cache(serveur: str) -> None: + """ + Vide le cache d'exécution de ComfyUI. + + INDISPENSABLE avant tout test de reproductibilité. ComfyUI met en cache le résultat + d'un graphe identique : relancer le même workflow ne recalcule rien et renvoie la + sortie précédente. Comparer deux « exécutions » sans vider le cache ne prouve donc + rien du tout — seulement que le cache fonctionne. + + Constaté en session 3 : la deuxième exécution n'avait écrit aucun fichier dans le + dossier de sortie de ComfyUI. Le hasard a voulu que la conclusion soit juste ; la + méthode, elle, était fausse. + """ + corps = json.dumps({"unload_models": True, "free_memory": True}).encode("utf-8") + requete = urllib.request.Request( + urllib.parse.urljoin(serveur, "/free"), + data=corps, + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(requete, timeout=30): + pass + + def poster(serveur: str, workflow: dict, client_id: str) -> str: """Envoie un workflow à ComfyUI et renvoie l'identifiant de la tâche.""" corps = json.dumps({"prompt": workflow, "client_id": client_id}).encode("utf-8") @@ -190,6 +213,41 @@ def generer(args: argparse.Namespace) -> int: return 0 +def reproductibilite(args: argparse.Namespace) -> int: + """ + Test de reproductibilité RÉEL : génère, vide le cache, régénère, compare. + + Sans le vidage intermédiaire, ce test ne mesure que le cache de ComfyUI. + """ + args_gen = argparse.Namespace(**vars(args)) + args_gen.sec = False + + print("— première génération") + generer(args_gen) + premier = json.loads((SORTIE / "manifeste.json").read_text(encoding="utf-8")) + + print("\n— vidage du cache d'exécution") + vider_cache(args.serveur) + + print("\n— seconde génération, cache vidé") + generer(args_gen) + second = json.loads((SORTIE / "manifeste.json").read_text(encoding="utf-8")) + + identiques = True + print() + for a, b in zip(premier, second): + egal = a["sha256"] == b["sha256"] + identiques &= egal + print(f"{'✔' if egal else '✘'} {a['cle']} — seed {a['seed']} — {a['sha256'][:24]}") + + print( + "\n✔ Sorties bit-identiques après vidage du cache : la reproductibilité est réelle." + if identiques + else "\n✘ Sorties divergentes : la reproductibilité n'est PAS acquise." + ) + return 0 if identiques else 1 + + def verifier(_args: argparse.Namespace) -> int: """ Contrôle que les seeds sont bien déterministes. @@ -218,10 +276,19 @@ def main() -> int: parseur.add_argument("--serveur", default=COMFY_DEFAUT) parseur.add_argument("--sec", action="store_true", help="calcule les seeds sans appeler ComfyUI") parseur.add_argument("--verifier", action="store_true", help="teste le déterminisme des seeds") + parseur.add_argument( + "--reproductibilite", + action="store_true", + help="génère deux fois en vidant le cache entre les deux, et compare les empreintes", + ) args = parseur.parse_args() if args.verifier: return verifier(args) + if args.reproductibilite: + if not args.workflow or not args.plan: + parseur.error("--workflow et --plan sont requis pour --reproductibilite") + return reproductibilite(args) if not args.workflow or not args.plan: parseur.error("--workflow et --plan sont requis (ou --verifier)") return generer(args)