[{"data":1,"prerenderedAt":1168},["ShallowReactive",2],{"blog-\u002Fblog\u002Fisoler-le-moteur-d-inference-pour-tester-sans-gpu":3,"blog-translation-\u002Fblog\u002Fisoler-le-moteur-d-inference-pour-tester-sans-gpu":1124,"blog-series-\u002Fblog\u002Fisoler-le-moteur-d-inference-pour-tester-sans-gpu":1147},{"id":4,"title":5,"author":6,"body":7,"category":1123,"cover":1124,"date":1125,"description":1126,"extension":1127,"featured":1128,"kind":1129,"language":1130,"meta":1131,"modified":1124,"navigation":161,"path":1132,"published":161,"readingTime":1133,"seo":1134,"seriesKey":1135,"seriesOrder":1136,"seriesTitle":1137,"sitemap":1138,"stem":1139,"tags":1140,"translationKey":1124,"__hash__":1146},"blog\u002Fblog\u002Fisoler-le-moteur-d-inference-pour-tester-sans-gpu.md","Isoler le moteur d'inférence pour tester sans GPU","Lucas Jahier",{"type":8,"value":9,"toc":1111},"minimark",[10,15,19,27,31,34,37,41,44,54,57,60,282,285,289,292,457,460,463,470,474,477,589,592,596,599,648,651,737,740,744,747,767,770,774,796,913,916,922,925,928,1026,1033,1039,1046,1056,1059,1067,1072,1083,1087,1094,1097,1101,1104,1107],[11,12,14],"h2",{"id":13},"résumé","Résumé",[16,17,18],"p",{},"Une plateforme générative contient beaucoup de code qui ne génère aucun\npixel. Pourtant, lorsque l'orchestration dépend directement d'une bibliothèque\nd'inférence, chaque test finit par exiger un checkpoint, un accélérateur et\nplusieurs minutes de chargement.",[16,20,21,22,26],{},"Dans ",[23,24,25],"code",{},"mlx-diffusion-lab",", l'orchestration produit un manifeste validé puis le\nconfie à un runner. Un moteur factice respecte la même frontière que le moteur\nMFLUX et produit des images déterministes sans poids. Il devient ainsi possible\nde tester l'API, la file, les sweeps, les fingerprints, la reprise et la retouche\nmasquée sans exécuter de modèle. Les tests logiciels rapides ne remplacent pas\nles smoke tests réels : ils leur réservent les propriétés que seuls les vrais\npoids peuvent confirmer.",[11,28,30],{"id":29},"_1-le-problème-nest-pas-seulement-le-temps-de-calcul","1. Le problème n'est pas seulement le temps de calcul",[16,32,33],{},"Validation des requêtes, calcul des fingerprints, admission dans une file,\ntransitions d'état, écritures atomiques, reprise après incident et présentation\ndes erreurs sont des comportements logiciels ordinaires. Aucun ne devrait avoir\nbesoin d'un modèle de diffusion pour être vérifié.",[16,35,36],{},"Si tous les tests chargent les poids, la boucle de développement devient lente,\ncoûteuse et ambiguë. Un échec peut provenir du réseau, des poids, de la mémoire\nou de l'orchestration sans que sa cause soit immédiatement visible. La solution\nconsiste à isoler la capacité minimale attendue du calcul : recevoir un travail\nrésolu, produire une image et conserver sa provenance.",[11,38,40],{"id":39},"_2-deux-chemins-convergent-vers-le-même-contrat","2. Deux chemins convergent vers le même contrat",[16,42,43],{},"Le serveur HTTP et l'outil de sweep ne suivent pas exactement le même chemin.\nL'API crée des jobs durables, les place dans une file et laisse un worker unique\nles exécuter. Le sweep construit directement une liste de manifestes et les\ntransmet au runner après avoir écarté les sorties déjà présentes.",[45,46,52],"pre",{"className":47,"code":49,"language":50,"meta":51},[48],"language-text","requête HTTP                    fichier de sweep\n     ↓                               ↓\nvalidation, job, file          validation, planification\n     ↓                               ↓\n   worker                        exécuteur du sweep\n     └─────────── manifeste ───────────┘\n                         ↓\n                  BaseRunner.run()\n                    ↙         ↘\n                  stub          MFLUX\n","text","",[23,53,49],{"__ignoreMap":51},[16,55,56],{},"Le manifeste contient le type de job, le profil, le prompt, la seed, les\ndimensions, les paramètres et, selon le cas, une image parente ou les réglages\ndu masque. Le runner ne choisit ni le prochain identifiant, ni la politique de\nreprise, ni le chemin de sortie : ces décisions sont déjà prises lorsqu'il reçoit\nle manifeste.",[16,58,59],{},"La classe commune prend en charge le dispatch, la mesure, l'écriture du PNG et\nle sidecar. Un backend n'implémente que l'opération de génération :",[45,61,65],{"className":62,"code":63,"language":64,"meta":51,"style":51},"language-python shiki shiki-themes github-dark","class BaseRunner(ABC):\n    @abstractmethod\n    def generate(\n        self, *, prompt, seed, steps, guidance, width, height,\n        image_path=None, image_strength=None,\n    ) -> Image.Image:\n        \"\"\"Produire une image PIL.\"\"\"\n\n    def run(self, manifest: Manifest) -> Result:\n        started = time.perf_counter()\n        image, metrics = self.render(manifest)\n        duration = round(time.perf_counter() - started, 3)\n        atomic_image(Path(manifest.output), image, format=\"PNG\")\n        sidecar = self.sidecar(manifest, metrics, duration)\n        atomic_json(Path(manifest.metadata), sidecar)\n        return Result(\n            Path(manifest.output), Path(manifest.metadata), duration, metrics\n        )\n","python",[23,66,67,91,97,109,121,143,149,156,163,174,185,199,225,242,255,261,270,276],{"__ignoreMap":51},[68,69,72,76,80,84,88],"span",{"class":70,"line":71},"line",1,[68,73,75],{"class":74},"snl16","class",[68,77,79],{"class":78},"svObZ"," BaseRunner",[68,81,83],{"class":82},"s95oV","(",[68,85,87],{"class":86},"sDLfK","ABC",[68,89,90],{"class":82},"):\n",[68,92,94],{"class":70,"line":93},2,[68,95,96],{"class":78},"    @abstractmethod\n",[68,98,100,103,106],{"class":70,"line":99},3,[68,101,102],{"class":74},"    def",[68,104,105],{"class":78}," generate",[68,107,108],{"class":82},"(\n",[68,110,112,115,118],{"class":70,"line":111},4,[68,113,114],{"class":82},"        self, ",[68,116,117],{"class":74},"*",[68,119,120],{"class":82},", prompt, seed, steps, guidance, width, height,\n",[68,122,124,127,130,133,136,138,140],{"class":70,"line":123},5,[68,125,126],{"class":82},"        image_path",[68,128,129],{"class":74},"=",[68,131,132],{"class":86},"None",[68,134,135],{"class":82},", image_strength",[68,137,129],{"class":74},[68,139,132],{"class":86},[68,141,142],{"class":82},",\n",[68,144,146],{"class":70,"line":145},6,[68,147,148],{"class":82},"    ) -> Image.Image:\n",[68,150,152],{"class":70,"line":151},7,[68,153,155],{"class":154},"sU2Wk","        \"\"\"Produire une image PIL.\"\"\"\n",[68,157,159],{"class":70,"line":158},8,[68,160,162],{"emptyLinePlaceholder":161},true,"\n",[68,164,166,168,171],{"class":70,"line":165},9,[68,167,102],{"class":74},[68,169,170],{"class":78}," run",[68,172,173],{"class":82},"(self, manifest: Manifest) -> Result:\n",[68,175,177,180,182],{"class":70,"line":176},10,[68,178,179],{"class":82},"        started ",[68,181,129],{"class":74},[68,183,184],{"class":82}," time.perf_counter()\n",[68,186,188,191,193,196],{"class":70,"line":187},11,[68,189,190],{"class":82},"        image, metrics ",[68,192,129],{"class":74},[68,194,195],{"class":86}," self",[68,197,198],{"class":82},".render(manifest)\n",[68,200,202,205,207,210,213,216,219,222],{"class":70,"line":201},12,[68,203,204],{"class":82},"        duration ",[68,206,129],{"class":74},[68,208,209],{"class":86}," round",[68,211,212],{"class":82},"(time.perf_counter() ",[68,214,215],{"class":74},"-",[68,217,218],{"class":82}," started, ",[68,220,221],{"class":86},"3",[68,223,224],{"class":82},")\n",[68,226,228,231,235,237,240],{"class":70,"line":227},13,[68,229,230],{"class":82},"        atomic_image(Path(manifest.output), image, ",[68,232,234],{"class":233},"s9osk","format",[68,236,129],{"class":74},[68,238,239],{"class":154},"\"PNG\"",[68,241,224],{"class":82},[68,243,245,248,250,252],{"class":70,"line":244},14,[68,246,247],{"class":82},"        sidecar ",[68,249,129],{"class":74},[68,251,195],{"class":86},[68,253,254],{"class":82},".sidecar(manifest, metrics, duration)\n",[68,256,258],{"class":70,"line":257},15,[68,259,260],{"class":82},"        atomic_json(Path(manifest.metadata), sidecar)\n",[68,262,264,267],{"class":70,"line":263},16,[68,265,266],{"class":74},"        return",[68,268,269],{"class":82}," Result(\n",[68,271,273],{"class":70,"line":272},17,[68,274,275],{"class":82},"            Path(manifest.output), Path(manifest.metadata), duration, metrics\n",[68,277,279],{"class":70,"line":278},18,[68,280,281],{"class":82},"        )\n",[16,283,284],{},"Le code réel mesure aussi la durée et vérifie que le profil du manifeste est\nbien celui servi par le processus. Le point important est ailleurs : la logique\ncommune d'écriture et de provenance ne peut pas diverger entre le stub et MFLUX.",[11,286,288],{"id":287},"_3-un-moteur-factice-qui-produit-de-linformation","3. Un moteur factice qui produit de l'information",[16,290,291],{},"Un stub utile ne renvoie pas une constante vide. Il construit un dégradé à\npartir du prompt, de la seed, des dimensions et des paramètres de génération,\npuis ajoute une légende lisible. Pour une édition, il mélange aussi l'image\nparente au résultat. Une planche contact factice reste donc exploitable pour\ninspecter un sweep et sa filiation.",[45,293,295],{"className":62,"code":294,"language":64,"meta":51,"style":51},"material = f\"{prompt}|{seed}|{steps}|{guidance}|{width}x{height}\"\nrng = np.random.default_rng(_stable_seed(material))\nimage = _gradient(width, height, rng)\n\nif image_path is not None:\n    source = Image.open(image_path).convert(\"RGB\")\n    source = source.resize((width, height), Image.Resampling.LANCZOS)\n    image = Image.blend(source, image, 0.45)\n",[23,296,297,369,379,389,393,413,428,442],{"__ignoreMap":51},[68,298,299,302,304,307,310,313,316,319,322,324,327,329,331,333,336,338,340,342,345,347,349,351,354,356,359,361,364,366],{"class":70,"line":71},[68,300,301],{"class":82},"material ",[68,303,129],{"class":74},[68,305,306],{"class":74}," f",[68,308,309],{"class":154},"\"",[68,311,312],{"class":86},"{",[68,314,315],{"class":82},"prompt",[68,317,318],{"class":86},"}",[68,320,321],{"class":154},"|",[68,323,312],{"class":86},[68,325,326],{"class":82},"seed",[68,328,318],{"class":86},[68,330,321],{"class":154},[68,332,312],{"class":86},[68,334,335],{"class":82},"steps",[68,337,318],{"class":86},[68,339,321],{"class":154},[68,341,312],{"class":86},[68,343,344],{"class":82},"guidance",[68,346,318],{"class":86},[68,348,321],{"class":154},[68,350,312],{"class":86},[68,352,353],{"class":82},"width",[68,355,318],{"class":86},[68,357,358],{"class":154},"x",[68,360,312],{"class":86},[68,362,363],{"class":82},"height",[68,365,318],{"class":86},[68,367,368],{"class":154},"\"\n",[68,370,371,374,376],{"class":70,"line":93},[68,372,373],{"class":82},"rng ",[68,375,129],{"class":74},[68,377,378],{"class":82}," np.random.default_rng(_stable_seed(material))\n",[68,380,381,384,386],{"class":70,"line":99},[68,382,383],{"class":82},"image ",[68,385,129],{"class":74},[68,387,388],{"class":82}," _gradient(width, height, rng)\n",[68,390,391],{"class":70,"line":111},[68,392,162],{"emptyLinePlaceholder":161},[68,394,395,398,401,404,407,410],{"class":70,"line":123},[68,396,397],{"class":74},"if",[68,399,400],{"class":82}," image_path ",[68,402,403],{"class":74},"is",[68,405,406],{"class":74}," not",[68,408,409],{"class":86}," None",[68,411,412],{"class":82},":\n",[68,414,415,418,420,423,426],{"class":70,"line":145},[68,416,417],{"class":82},"    source ",[68,419,129],{"class":74},[68,421,422],{"class":82}," Image.open(image_path).convert(",[68,424,425],{"class":154},"\"RGB\"",[68,427,224],{"class":82},[68,429,430,432,434,437,440],{"class":70,"line":151},[68,431,417],{"class":82},[68,433,129],{"class":74},[68,435,436],{"class":82}," source.resize((width, height), Image.Resampling.",[68,438,439],{"class":86},"LANCZOS",[68,441,224],{"class":82},[68,443,444,447,449,452,455],{"class":70,"line":158},[68,445,446],{"class":82},"    image ",[68,448,129],{"class":74},[68,450,451],{"class":82}," Image.blend(source, image, ",[68,453,454],{"class":86},"0.45",[68,456,224],{"class":82},[16,458,459],{},"Deux exécutions identiques produisent le même PNG octet par octet et deux seeds\ndifférentes produisent des images différentes. Cette identité ne s'étend pas au\nsidecar : son horodatage et sa durée changent naturellement entre deux passages.",[16,461,462],{},"Le nom du runner entre dans le fingerprint. Un placeholder créé par le stub ne\npeut donc pas occuper le nom ou le cache d'une future sortie MFLUX.",[16,464,465,466,469],{},"Le stub a aussi des limites assumées. Il ne reproduit pas la sensibilité du vrai\nmoteur à tous les paramètres, notamment ",[23,467,468],{},"image_strength",", et son déterminisme ne\nprédit pas celui de MFLUX sur plusieurs machines. Il vérifie le comportement de\nl'orchestration, pas la physique du modèle.",[11,471,473],{"id":472},"_4-ne-pas-charger-ce-que-lon-ne-demande-pas","4. Ne pas charger ce que l'on ne demande pas",[16,475,476],{},"Le découplage serait incomplet si importer le package chargeait tout de même MLX,\nMFLUX ou Torch. Les dépendances lourdes sont donc des extras optionnels et le\nrunner réel n'est importé que lorsqu'il est explicitement choisi :",[45,478,480],{"className":62,"code":479,"language":64,"meta":51,"style":51},"def build_runner(profile, name):\n    if name == \"stub\":\n        from imagegen.runners.stub import StubRunner\n        return StubRunner(profile)\n    if name == \"mflux\":\n        from imagegen.runners.mflux import MfluxRunner\n        return MfluxRunner(profile)\n    raise ValueError(f\"unknown runner {name}\")\n",[23,481,482,493,509,523,530,543,555,562],{"__ignoreMap":51},[68,483,484,487,490],{"class":70,"line":71},[68,485,486],{"class":74},"def",[68,488,489],{"class":78}," build_runner",[68,491,492],{"class":82},"(profile, name):\n",[68,494,495,498,501,504,507],{"class":70,"line":93},[68,496,497],{"class":74},"    if",[68,499,500],{"class":82}," name ",[68,502,503],{"class":74},"==",[68,505,506],{"class":154}," \"stub\"",[68,508,412],{"class":82},[68,510,511,514,517,520],{"class":70,"line":99},[68,512,513],{"class":74},"        from",[68,515,516],{"class":82}," imagegen.runners.stub ",[68,518,519],{"class":74},"import",[68,521,522],{"class":82}," StubRunner\n",[68,524,525,527],{"class":70,"line":111},[68,526,266],{"class":74},[68,528,529],{"class":82}," StubRunner(profile)\n",[68,531,532,534,536,538,541],{"class":70,"line":123},[68,533,497],{"class":74},[68,535,500],{"class":82},[68,537,503],{"class":74},[68,539,540],{"class":154}," \"mflux\"",[68,542,412],{"class":82},[68,544,545,547,550,552],{"class":70,"line":145},[68,546,513],{"class":74},[68,548,549],{"class":82}," imagegen.runners.mflux ",[68,551,519],{"class":74},[68,553,554],{"class":82}," MfluxRunner\n",[68,556,557,559],{"class":70,"line":151},[68,558,266],{"class":74},[68,560,561],{"class":82}," MfluxRunner(profile)\n",[68,563,564,567,570,572,575,578,580,583,585,587],{"class":70,"line":158},[68,565,566],{"class":74},"    raise",[68,568,569],{"class":86}," ValueError",[68,571,83],{"class":82},[68,573,574],{"class":74},"f",[68,576,577],{"class":154},"\"unknown runner ",[68,579,312],{"class":86},[68,581,582],{"class":82},"name",[68,584,318],{"class":86},[68,586,309],{"class":154},[68,588,224],{"class":82},[16,590,591],{},"Le même principe s'applique à la segmentation. Le segmenter n'est construit\nqu'au premier job masqué. Les tests utilisent un segmenter géométrique factice ;\nCLIPSeg, Transformers et Torch ne sont pas nécessaires pour tester les crops, le\nbruit déterministe et la recomposition.",[11,593,595],{"id":594},"_5-ce-qui-devient-testable-sans-modèle","5. Ce qui devient testable sans modèle",[16,597,598],{},"Cette séparation couvre notamment :",[600,601,602,606,609,612,615,636,639,642,645],"ul",{},[603,604,605],"li",{},"la validation stricte des manifestes ;",[603,607,608],{},"la planification et la reprise des sweeps ;",[603,610,611],{},"la déduplication par fingerprint ;",[603,613,614],{},"les réponses HTTP et la profondeur de la file ;",[603,616,617,618,621,622,621,625,621,628,631,632,635],{},"les statuts ",[23,619,620],{},"queued",", ",[23,623,624],{},"running",[23,626,627],{},"done",[23,629,630],{},"failed"," et ",[23,633,634],{},"interrupted"," ;",[603,637,638],{},"le journal append-only et sa relecture après redémarrage ;",[603,640,641],{},"la production de planches contact ;",[603,643,644],{},"les crops, la segmentation factice et la recomposition ;",[603,646,647],{},"l'invariant qui impose un delta nul hors du masque.",[16,649,650],{},"Un test du stub exprime directement le contrat de déterminisme :",[45,652,654],{"className":62,"code":653,"language":64,"meta":51,"style":51},"def test_the_same_job_twice_is_byte_identical(tmp_path, profile):\n    first = make_manifest(tmp_path, output=tmp_path \u002F \"a.png\")\n    second = make_manifest(tmp_path, output=tmp_path \u002F \"b.png\")\n    run(profile, first)\n    run(profile, second)\n    assert first.output.read_bytes() == second.output.read_bytes()\n",[23,655,656,666,692,714,719,724],{"__ignoreMap":51},[68,657,658,660,663],{"class":70,"line":71},[68,659,486],{"class":74},[68,661,662],{"class":78}," test_the_same_job_twice_is_byte_identical",[68,664,665],{"class":82},"(tmp_path, profile):\n",[68,667,668,671,673,676,679,681,684,687,690],{"class":70,"line":93},[68,669,670],{"class":82},"    first ",[68,672,129],{"class":74},[68,674,675],{"class":82}," make_manifest(tmp_path, ",[68,677,678],{"class":233},"output",[68,680,129],{"class":74},[68,682,683],{"class":82},"tmp_path ",[68,685,686],{"class":74},"\u002F",[68,688,689],{"class":154}," \"a.png\"",[68,691,224],{"class":82},[68,693,694,697,699,701,703,705,707,709,712],{"class":70,"line":99},[68,695,696],{"class":82},"    second ",[68,698,129],{"class":74},[68,700,675],{"class":82},[68,702,678],{"class":233},[68,704,129],{"class":74},[68,706,683],{"class":82},[68,708,686],{"class":74},[68,710,711],{"class":154}," \"b.png\"",[68,713,224],{"class":82},[68,715,716],{"class":70,"line":111},[68,717,718],{"class":82},"    run(profile, first)\n",[68,720,721],{"class":70,"line":123},[68,722,723],{"class":82},"    run(profile, second)\n",[68,725,726,729,732,734],{"class":70,"line":145},[68,727,728],{"class":74},"    assert",[68,730,731],{"class":82}," first.output.read_bytes() ",[68,733,503],{"class":74},[68,735,736],{"class":82}," second.output.read_bytes()\n",[16,738,739],{},"Les écritures sont atomiques individuellement : chaque fichier temporaire est\nrenommé sur le même système de fichiers une fois complet. Un lecteur ne voit donc\njamais un PNG ou un JSON à moitié écrit. Cela ne constitue toutefois pas une\ntransaction englobant le PNG et son sidecar : une panne entre les deux écritures\npeut laisser une image complète sans ses métadonnées.",[11,741,743],{"id":742},"_6-ce-que-seul-le-moteur-réel-peut-confirmer","6. Ce que seul le moteur réel peut confirmer",[16,745,746],{},"Un double de test ne peut pas prouver :",[600,748,749,752,755,758,761,764],{},[603,750,751],{},"le chargement effectif du checkpoint et d'un encodeur de remplacement ;",[603,753,754],{},"l'application des LoRA dans le bon ordre ;",[603,756,757],{},"la compatibilité entre l'architecture et les adaptateurs ;",[603,759,760],{},"la qualité de la segmentation sur des images réelles ;",[603,762,763],{},"la consommation de mémoire et le débit ;",[603,765,766],{},"la qualité visuelle du résultat.",[16,768,769],{},"Ces propriétés appartiennent à une petite suite de smoke tests matériels. Une\nimage réussie ne remplace pas les tests logiciels ; elle valide seulement le\nchemin d'inférence réel utilisé pour la produire.",[11,771,773],{"id":772},"_7-une-vérification-reproductible","7. Une vérification reproductible",[16,775,776,777,787,788,791,792,795],{},"La ",[778,779,783,784],"a",{"href":780,"rel":781},"https:\u002F\u002Fgithub.com\u002Fstratorys\u002Fmlx-diffusion-lab\u002Ftree\u002F4720537",[782],"nofollow","révision ",[23,785,786],{},"4720537","\ndu dépôt contient 144 tests. Ils ont été exécutés dans un environnement Python\n3.14.4 neuf contenant uniquement les dépendances standard et l'extra ",[23,789,790],{},"dev",". Une\nvérification avec ",[23,793,794],{},"importlib.util.find_spec"," y a confirmé l'absence de Torch,\nMFLUX, MLX et Transformers.",[45,797,801],{"className":798,"code":799,"language":800,"meta":51,"style":51},"language-bash shiki shiki-themes github-dark","UV_PROJECT_ENVIRONMENT=\u002Ftmp\u002Fimagegen-dev-only \\\n  uv sync --extra dev\n\nUV_PROJECT_ENVIRONMENT=\u002Ftmp\u002Fimagegen-dev-only \\\n  uv run python -c \\\n  \"import importlib.util; print({n: importlib.util.find_spec(n) is not None for n in ('torch', 'mflux', 'mlx', 'transformers')})\"\n\nUV_PROJECT_ENVIRONMENT=\u002Ftmp\u002Fimagegen-dev-only uv run pytest -q\nUV_PROJECT_ENVIRONMENT=\u002Ftmp\u002Fimagegen-dev-only uv run ruff check src tests bench\n","bash",[23,802,803,816,830,834,844,858,863,867,886],{"__ignoreMap":51},[68,804,805,808,810,813],{"class":70,"line":71},[68,806,807],{"class":82},"UV_PROJECT_ENVIRONMENT",[68,809,129],{"class":74},[68,811,812],{"class":154},"\u002Ftmp\u002Fimagegen-dev-only",[68,814,815],{"class":78}," \\\n",[68,817,818,821,824,827],{"class":70,"line":93},[68,819,820],{"class":154},"  uv",[68,822,823],{"class":154}," sync",[68,825,826],{"class":86}," --extra",[68,828,829],{"class":154}," dev\n",[68,831,832],{"class":70,"line":99},[68,833,162],{"emptyLinePlaceholder":161},[68,835,836,838,840,842],{"class":70,"line":111},[68,837,807],{"class":82},[68,839,129],{"class":74},[68,841,812],{"class":154},[68,843,815],{"class":78},[68,845,846,848,850,853,856],{"class":70,"line":123},[68,847,820],{"class":154},[68,849,170],{"class":154},[68,851,852],{"class":154}," python",[68,854,855],{"class":86}," -c",[68,857,815],{"class":86},[68,859,860],{"class":70,"line":145},[68,861,862],{"class":154},"  \"import importlib.util; print({n: importlib.util.find_spec(n) is not None for n in ('torch', 'mflux', 'mlx', 'transformers')})\"\n",[68,864,865],{"class":70,"line":151},[68,866,162],{"emptyLinePlaceholder":161},[68,868,869,871,873,875,878,880,883],{"class":70,"line":158},[68,870,807],{"class":82},[68,872,129],{"class":74},[68,874,812],{"class":154},[68,876,877],{"class":78}," uv",[68,879,170],{"class":154},[68,881,882],{"class":154}," pytest",[68,884,885],{"class":86}," -q\n",[68,887,888,890,892,894,896,898,901,904,907,910],{"class":70,"line":165},[68,889,807],{"class":82},[68,891,129],{"class":74},[68,893,812],{"class":154},[68,895,877],{"class":78},[68,897,170],{"class":154},[68,899,900],{"class":154}," ruff",[68,902,903],{"class":154}," check",[68,905,906],{"class":154}," src",[68,908,909],{"class":154}," tests",[68,911,912],{"class":154}," bench\n",[16,914,915],{},"Le 14 septembre 2026, cette procédure a produit le résultat suivant :",[45,917,920],{"className":918,"code":919,"language":50,"meta":51},[48],"{'torch': False, 'mflux': False, 'mlx': False, 'transformers': False}\n144 passed, 2 warnings in 2.97s\nAll checks passed!\n",[23,921,919],{"__ignoreMap":51},[16,923,924],{},"Les deux avertissements proviennent de dépréciations dans les outils de test de\nFastAPI et Starlette ; ils ne signalent ni un test ignoré ni un échec. Installer\nles dépendances peut demander le réseau, mais exécuter cette suite n'en a pas\nbesoin une fois l'environnement construit.",[16,926,927],{},"Un smoke test réel minimal utilise un environnement distinct :",[45,929,931],{"className":798,"code":930,"language":800,"meta":51,"style":51},"UV_PROJECT_ENVIRONMENT=\u002Ftmp\u002Fimagegen-mflux-smoke \\\n  uv sync --extra dev --extra mflux\n\nUV_PROJECT_ENVIRONMENT=\u002Ftmp\u002Fimagegen-mflux-smoke \\\n  uv run imagegen-sweep sweeps\u002Fexample.json \\\n    --profile plain-4b \\\n    --runner mflux \\\n    --limit 1 \\\n    --out \u002Ftmp\u002Fimagegen-real-smoke-20260914\n",[23,932,933,944,960,964,974,988,998,1008,1018],{"__ignoreMap":51},[68,934,935,937,939,942],{"class":70,"line":71},[68,936,807],{"class":82},[68,938,129],{"class":74},[68,940,941],{"class":154},"\u002Ftmp\u002Fimagegen-mflux-smoke",[68,943,815],{"class":78},[68,945,946,948,950,952,955,957],{"class":70,"line":93},[68,947,820],{"class":154},[68,949,823],{"class":154},[68,951,826],{"class":86},[68,953,954],{"class":154}," dev",[68,956,826],{"class":86},[68,958,959],{"class":154}," mflux\n",[68,961,962],{"class":70,"line":99},[68,963,162],{"emptyLinePlaceholder":161},[68,965,966,968,970,972],{"class":70,"line":111},[68,967,807],{"class":82},[68,969,129],{"class":74},[68,971,941],{"class":154},[68,973,815],{"class":78},[68,975,976,978,980,983,986],{"class":70,"line":123},[68,977,820],{"class":154},[68,979,170],{"class":154},[68,981,982],{"class":154}," imagegen-sweep",[68,984,985],{"class":154}," sweeps\u002Fexample.json",[68,987,815],{"class":86},[68,989,990,993,996],{"class":70,"line":145},[68,991,992],{"class":86},"    --profile",[68,994,995],{"class":154}," plain-4b",[68,997,815],{"class":86},[68,999,1000,1003,1006],{"class":70,"line":151},[68,1001,1002],{"class":86},"    --runner",[68,1004,1005],{"class":154}," mflux",[68,1007,815],{"class":86},[68,1009,1010,1013,1016],{"class":70,"line":158},[68,1011,1012],{"class":86},"    --limit",[68,1014,1015],{"class":86}," 1",[68,1017,815],{"class":86},[68,1019,1020,1023],{"class":70,"line":165},[68,1021,1022],{"class":86},"    --out",[68,1024,1025],{"class":154}," \u002Ftmp\u002Fimagegen-real-smoke-20260914\n",[16,1027,1028,1029,1032],{},"Sur une machine macOS arm64 avec MFLUX 0.19.1 et MLX 0.32.2, le runner a chargé\nle checkpoint public ",[23,1030,1031],{},"mlx-community\u002Fflux2-klein-4b-4bit"," en 3,5 secondes. Il a\nensuite produit en 24,65 secondes une image de 768 × 1024 pixels, son manifeste,\nson sidecar et l'index du sweep :",[45,1034,1037],{"className":1035,"code":1036,"language":50,"meta":51},[48],"runner mflux ready in 3.5s\n[1\u002F1] harbour seed 1  harbour_45b9d36c0e_seed1.png  24.65s\n1 produced, 0 failed\n",[23,1038,1036],{"__ignoreMap":51},[16,1040,1041],{},[1042,1043],"img",{"alt":1044,"src":1045},"Port de pierre généré par le smoke test MFLUX","\u002Fassets\u002Fblog\u002Ftester-inference-sans-gpu\u002Fharbour-smoke-test.png",[16,1047,1048],{},[1049,1050,1051,1052,1055],"em",{},"Sortie brute du smoke test, sans retouche ni sélection parmi plusieurs\nvariantes. Le sweep planifiait sept images, mais ",[23,1053,1054],{},"--limit 1"," n'en a exécuté\nqu'une.",[16,1057,1058],{},"Le manifeste conserve les entrées du calcul. Le sidecar ajoute l'identité\neffective du moteur et la mesure de l'exécution. Les deux artefacts complets\npeuvent être consultés ci-dessous.",[1060,1061],"json-artifact",{"copied-label":1062,"copy-label":1063,"error-label":1064,"file":1065,"title":1066},"Copié","Copier","Le manifeste JSON n’a pas pu être chargé.","tester-inference-sans-gpu\u002Fharbour-smoke-test.manifest.json","Manifeste résolu",[1060,1068],{"copied-label":1062,"copy-label":1063,"error-label":1069,"file":1070,"title":1071},"Le sidecar JSON n’a pas pu être chargé.","tester-inference-sans-gpu\u002Fharbour-smoke-test.metadata.json","Sidecar complet",[16,1073,1074,1075,1078,1079,1082],{},"Le sidecar confirme le runner ",[23,1076,1077],{},"mflux",", le profil ",[23,1080,1081],{},"plain-4b",", quatre pas, une\nguidance de 1,0 et l'absence d'adaptateur. Ce smoke test ne vérifie donc ni les\nLoRA, ni l'encodeur de remplacement, ni CLIPSeg : chacun exige un cas de test\nréel supplémentaire.",[11,1084,1086],{"id":1085},"_8-ajouter-un-autre-moteur","8. Ajouter un autre moteur",[16,1088,1089,1090,1093],{},"Un nouveau backend doit charger ses ressources et implémenter ",[23,1091,1092],{},"generate(...)","\npour retourner une image PIL. Le traitement du manifeste, le masquage, les\nécritures et la provenance restent dans la classe commune ; l'API, la file et\nle planificateur de sweeps n'ont pas à changer.",[16,1095,1096],{},"Cette promesse possède néanmoins une condition : le contrat ne doit pas être\nimplicitement calqué sur MFLUX. L'ajout d'un second moteur réel serait le test le\nplus fort de sa généralité. En attendant, le stub prouve surtout que\nl'orchestration n'est pas couplée au chargement de MFLUX.",[11,1098,1100],{"id":1099},"_9-conclusion","9. Conclusion",[16,1102,1103],{},"Une frontière étroite entre orchestration et inférence transforme la\ntestabilité d'une plateforme générative. Le stub rend rapides et déterministes\nles tests des comportements logiciels ; le moteur réel se concentre sur les\npropriétés matérielles et visuelles qu'aucun double ne peut simuler.",[16,1105,1106],{},"Cette séparation ne sert pas seulement les tests. Elle prépare aussi la\nsubstitution d'un backend et l'exécution dans un processus distinct. Distribuer\nle calcul entre plusieurs machines demande encore d'expliciter le transport,\nles capacités et les pannes réseau : c'est l'étape suivante, pas une propriété\ndéjà acquise par le seul contrat local.",[1108,1109,1110],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":51,"searchDepth":93,"depth":93,"links":1112},[1113,1114,1115,1116,1117,1118,1119,1120,1121,1122],{"id":13,"depth":93,"text":14},{"id":29,"depth":93,"text":30},{"id":39,"depth":93,"text":40},{"id":287,"depth":93,"text":288},{"id":472,"depth":93,"text":473},{"id":594,"depth":93,"text":595},{"id":742,"depth":93,"text":743},{"id":772,"depth":93,"text":773},{"id":1085,"depth":93,"text":1086},{"id":1099,"depth":93,"text":1100},"Reliable AI Systems",null,"2026-09-14","Un contrat commun et un moteur factice déterministe pour tester l'orchestration indépendamment des poids.","md",false,"research-note","fr",{},"\u002Fblog\u002Fisoler-le-moteur-d-inference-pour-tester-sans-gpu","8 min de lecture",{"title":5,"description":1126},"reliable-generative-images","6","Construire des pipelines d’images fiables",{"loc":1132},"blog\u002Fisoler-le-moteur-d-inference-pour-tester-sans-gpu",[1141,1142,1143,1144,1145],"software architecture","testing","dependency inversion","inference","deterministic systems","ycfBcytgrbYbYiEYjgZ6vF25p1UC6BmJ4naEXoRCQ_A",[1148,1152,1156,1159,1163,1167],{"title":1149,"path":1150,"published":1128,"seriesOrder":1151},"Pourquoi un prompt ne suffit pas à créer une identité visuelle cohérente","\u002Fblog\u002Fpourquoi-un-prompt-ne-suffit-pas-identite-visuelle-coherente","1",{"title":1153,"path":1154,"published":1128,"seriesOrder":1155},"Un dataset d'entraînement n'est pas un dossier d'images","\u002Fblog\u002Fun-dataset-d-entrainement-n-est-pas-un-dossier-d-images","2",{"title":1157,"path":1158,"published":1128,"seriesOrder":221},"Traiter une génération d'image comme un build reproductible","\u002Fblog\u002Ftraiter-une-generation-d-image-comme-un-build-reproductible",{"title":1160,"path":1161,"published":1128,"seriesOrder":1162},"Retoucher une image générée sans toucher au reste","\u002Fblog\u002Fretoucher-une-image-genere-sans-toucher-au-reste","4",{"title":1164,"path":1165,"published":1128,"seriesOrder":1166},"D'une histoire à un storyboard reproductible","\u002Fblog\u002Fd-une-histoire-a-un-storyboard-reproductible","5",{"title":5,"path":1132,"published":161,"seriesOrder":1136},1789350299205]