[{"data":1,"prerenderedAt":1349},["ShallowReactive",2],{"blog-list":3},[4,1150],{"id":5,"title":6,"author":7,"body":8,"category":1124,"cover":1125,"date":1126,"description":1127,"extension":1128,"featured":1129,"kind":1130,"language":1131,"meta":1132,"modified":1125,"navigation":162,"path":1133,"published":162,"readingTime":1134,"seo":1135,"seriesKey":1136,"seriesOrder":1137,"seriesTitle":1138,"sitemap":1139,"stem":1142,"tags":1143,"translationKey":1125,"__hash__":1149},"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":9,"value":10,"toc":1112},"minimark",[11,16,20,28,32,35,38,42,45,55,58,61,283,286,290,293,458,461,464,471,475,478,590,593,597,600,649,652,738,741,745,748,768,771,775,797,914,917,923,926,929,1027,1034,1040,1047,1057,1060,1068,1073,1084,1088,1095,1098,1102,1105,1108],[12,13,15],"h2",{"id":14},"résumé","Résumé",[17,18,19],"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.",[17,21,22,23,27],{},"Dans ",[24,25,26],"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.",[12,29,31],{"id":30},"_1-le-problème-nest-pas-seulement-le-temps-de-calcul","1. Le problème n'est pas seulement le temps de calcul",[17,33,34],{},"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é.",[17,36,37],{},"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.",[12,39,41],{"id":40},"_2-deux-chemins-convergent-vers-le-même-contrat","2. Deux chemins convergent vers le même contrat",[17,43,44],{},"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.",[46,47,53],"pre",{"className":48,"code":50,"language":51,"meta":52},[49],"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","",[24,54,50],{"__ignoreMap":52},[17,56,57],{},"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.",[17,59,60],{},"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 :",[46,62,66],{"className":63,"code":64,"language":65,"meta":52,"style":52},"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",[24,67,68,92,98,110,122,144,150,157,164,175,186,200,226,243,256,262,271,277],{"__ignoreMap":52},[69,70,73,77,81,85,89],"span",{"class":71,"line":72},"line",1,[69,74,76],{"class":75},"snl16","class",[69,78,80],{"class":79},"svObZ"," BaseRunner",[69,82,84],{"class":83},"s95oV","(",[69,86,88],{"class":87},"sDLfK","ABC",[69,90,91],{"class":83},"):\n",[69,93,95],{"class":71,"line":94},2,[69,96,97],{"class":79},"    @abstractmethod\n",[69,99,101,104,107],{"class":71,"line":100},3,[69,102,103],{"class":75},"    def",[69,105,106],{"class":79}," generate",[69,108,109],{"class":83},"(\n",[69,111,113,116,119],{"class":71,"line":112},4,[69,114,115],{"class":83},"        self, ",[69,117,118],{"class":75},"*",[69,120,121],{"class":83},", prompt, seed, steps, guidance, width, height,\n",[69,123,125,128,131,134,137,139,141],{"class":71,"line":124},5,[69,126,127],{"class":83},"        image_path",[69,129,130],{"class":75},"=",[69,132,133],{"class":87},"None",[69,135,136],{"class":83},", image_strength",[69,138,130],{"class":75},[69,140,133],{"class":87},[69,142,143],{"class":83},",\n",[69,145,147],{"class":71,"line":146},6,[69,148,149],{"class":83},"    ) -> Image.Image:\n",[69,151,153],{"class":71,"line":152},7,[69,154,156],{"class":155},"sU2Wk","        \"\"\"Produire une image PIL.\"\"\"\n",[69,158,160],{"class":71,"line":159},8,[69,161,163],{"emptyLinePlaceholder":162},true,"\n",[69,165,167,169,172],{"class":71,"line":166},9,[69,168,103],{"class":75},[69,170,171],{"class":79}," run",[69,173,174],{"class":83},"(self, manifest: Manifest) -> Result:\n",[69,176,178,181,183],{"class":71,"line":177},10,[69,179,180],{"class":83},"        started ",[69,182,130],{"class":75},[69,184,185],{"class":83}," time.perf_counter()\n",[69,187,189,192,194,197],{"class":71,"line":188},11,[69,190,191],{"class":83},"        image, metrics ",[69,193,130],{"class":75},[69,195,196],{"class":87}," self",[69,198,199],{"class":83},".render(manifest)\n",[69,201,203,206,208,211,214,217,220,223],{"class":71,"line":202},12,[69,204,205],{"class":83},"        duration ",[69,207,130],{"class":75},[69,209,210],{"class":87}," round",[69,212,213],{"class":83},"(time.perf_counter() ",[69,215,216],{"class":75},"-",[69,218,219],{"class":83}," started, ",[69,221,222],{"class":87},"3",[69,224,225],{"class":83},")\n",[69,227,229,232,236,238,241],{"class":71,"line":228},13,[69,230,231],{"class":83},"        atomic_image(Path(manifest.output), image, ",[69,233,235],{"class":234},"s9osk","format",[69,237,130],{"class":75},[69,239,240],{"class":155},"\"PNG\"",[69,242,225],{"class":83},[69,244,246,249,251,253],{"class":71,"line":245},14,[69,247,248],{"class":83},"        sidecar ",[69,250,130],{"class":75},[69,252,196],{"class":87},[69,254,255],{"class":83},".sidecar(manifest, metrics, duration)\n",[69,257,259],{"class":71,"line":258},15,[69,260,261],{"class":83},"        atomic_json(Path(manifest.metadata), sidecar)\n",[69,263,265,268],{"class":71,"line":264},16,[69,266,267],{"class":75},"        return",[69,269,270],{"class":83}," Result(\n",[69,272,274],{"class":71,"line":273},17,[69,275,276],{"class":83},"            Path(manifest.output), Path(manifest.metadata), duration, metrics\n",[69,278,280],{"class":71,"line":279},18,[69,281,282],{"class":83},"        )\n",[17,284,285],{},"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.",[12,287,289],{"id":288},"_3-un-moteur-factice-qui-produit-de-linformation","3. Un moteur factice qui produit de l'information",[17,291,292],{},"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.",[46,294,296],{"className":63,"code":295,"language":65,"meta":52,"style":52},"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",[24,297,298,370,380,390,394,414,429,443],{"__ignoreMap":52},[69,299,300,303,305,308,311,314,317,320,323,325,328,330,332,334,337,339,341,343,346,348,350,352,355,357,360,362,365,367],{"class":71,"line":72},[69,301,302],{"class":83},"material ",[69,304,130],{"class":75},[69,306,307],{"class":75}," f",[69,309,310],{"class":155},"\"",[69,312,313],{"class":87},"{",[69,315,316],{"class":83},"prompt",[69,318,319],{"class":87},"}",[69,321,322],{"class":155},"|",[69,324,313],{"class":87},[69,326,327],{"class":83},"seed",[69,329,319],{"class":87},[69,331,322],{"class":155},[69,333,313],{"class":87},[69,335,336],{"class":83},"steps",[69,338,319],{"class":87},[69,340,322],{"class":155},[69,342,313],{"class":87},[69,344,345],{"class":83},"guidance",[69,347,319],{"class":87},[69,349,322],{"class":155},[69,351,313],{"class":87},[69,353,354],{"class":83},"width",[69,356,319],{"class":87},[69,358,359],{"class":155},"x",[69,361,313],{"class":87},[69,363,364],{"class":83},"height",[69,366,319],{"class":87},[69,368,369],{"class":155},"\"\n",[69,371,372,375,377],{"class":71,"line":94},[69,373,374],{"class":83},"rng ",[69,376,130],{"class":75},[69,378,379],{"class":83}," np.random.default_rng(_stable_seed(material))\n",[69,381,382,385,387],{"class":71,"line":100},[69,383,384],{"class":83},"image ",[69,386,130],{"class":75},[69,388,389],{"class":83}," _gradient(width, height, rng)\n",[69,391,392],{"class":71,"line":112},[69,393,163],{"emptyLinePlaceholder":162},[69,395,396,399,402,405,408,411],{"class":71,"line":124},[69,397,398],{"class":75},"if",[69,400,401],{"class":83}," image_path ",[69,403,404],{"class":75},"is",[69,406,407],{"class":75}," not",[69,409,410],{"class":87}," None",[69,412,413],{"class":83},":\n",[69,415,416,419,421,424,427],{"class":71,"line":146},[69,417,418],{"class":83},"    source ",[69,420,130],{"class":75},[69,422,423],{"class":83}," Image.open(image_path).convert(",[69,425,426],{"class":155},"\"RGB\"",[69,428,225],{"class":83},[69,430,431,433,435,438,441],{"class":71,"line":152},[69,432,418],{"class":83},[69,434,130],{"class":75},[69,436,437],{"class":83}," source.resize((width, height), Image.Resampling.",[69,439,440],{"class":87},"LANCZOS",[69,442,225],{"class":83},[69,444,445,448,450,453,456],{"class":71,"line":159},[69,446,447],{"class":83},"    image ",[69,449,130],{"class":75},[69,451,452],{"class":83}," Image.blend(source, image, ",[69,454,455],{"class":87},"0.45",[69,457,225],{"class":83},[17,459,460],{},"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.",[17,462,463],{},"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.",[17,465,466,467,470],{},"Le stub a aussi des limites assumées. Il ne reproduit pas la sensibilité du vrai\nmoteur à tous les paramètres, notamment ",[24,468,469],{},"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.",[12,472,474],{"id":473},"_4-ne-pas-charger-ce-que-lon-ne-demande-pas","4. Ne pas charger ce que l'on ne demande pas",[17,476,477],{},"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 :",[46,479,481],{"className":63,"code":480,"language":65,"meta":52,"style":52},"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",[24,482,483,494,510,524,531,544,556,563],{"__ignoreMap":52},[69,484,485,488,491],{"class":71,"line":72},[69,486,487],{"class":75},"def",[69,489,490],{"class":79}," build_runner",[69,492,493],{"class":83},"(profile, name):\n",[69,495,496,499,502,505,508],{"class":71,"line":94},[69,497,498],{"class":75},"    if",[69,500,501],{"class":83}," name ",[69,503,504],{"class":75},"==",[69,506,507],{"class":155}," \"stub\"",[69,509,413],{"class":83},[69,511,512,515,518,521],{"class":71,"line":100},[69,513,514],{"class":75},"        from",[69,516,517],{"class":83}," imagegen.runners.stub ",[69,519,520],{"class":75},"import",[69,522,523],{"class":83}," StubRunner\n",[69,525,526,528],{"class":71,"line":112},[69,527,267],{"class":75},[69,529,530],{"class":83}," StubRunner(profile)\n",[69,532,533,535,537,539,542],{"class":71,"line":124},[69,534,498],{"class":75},[69,536,501],{"class":83},[69,538,504],{"class":75},[69,540,541],{"class":155}," \"mflux\"",[69,543,413],{"class":83},[69,545,546,548,551,553],{"class":71,"line":146},[69,547,514],{"class":75},[69,549,550],{"class":83}," imagegen.runners.mflux ",[69,552,520],{"class":75},[69,554,555],{"class":83}," MfluxRunner\n",[69,557,558,560],{"class":71,"line":152},[69,559,267],{"class":75},[69,561,562],{"class":83}," MfluxRunner(profile)\n",[69,564,565,568,571,573,576,579,581,584,586,588],{"class":71,"line":159},[69,566,567],{"class":75},"    raise",[69,569,570],{"class":87}," ValueError",[69,572,84],{"class":83},[69,574,575],{"class":75},"f",[69,577,578],{"class":155},"\"unknown runner ",[69,580,313],{"class":87},[69,582,583],{"class":83},"name",[69,585,319],{"class":87},[69,587,310],{"class":155},[69,589,225],{"class":83},[17,591,592],{},"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.",[12,594,596],{"id":595},"_5-ce-qui-devient-testable-sans-modèle","5. Ce qui devient testable sans modèle",[17,598,599],{},"Cette séparation couvre notamment :",[601,602,603,607,610,613,616,637,640,643,646],"ul",{},[604,605,606],"li",{},"la validation stricte des manifestes ;",[604,608,609],{},"la planification et la reprise des sweeps ;",[604,611,612],{},"la déduplication par fingerprint ;",[604,614,615],{},"les réponses HTTP et la profondeur de la file ;",[604,617,618,619,622,623,622,626,622,629,632,633,636],{},"les statuts ",[24,620,621],{},"queued",", ",[24,624,625],{},"running",[24,627,628],{},"done",[24,630,631],{},"failed"," et ",[24,634,635],{},"interrupted"," ;",[604,638,639],{},"le journal append-only et sa relecture après redémarrage ;",[604,641,642],{},"la production de planches contact ;",[604,644,645],{},"les crops, la segmentation factice et la recomposition ;",[604,647,648],{},"l'invariant qui impose un delta nul hors du masque.",[17,650,651],{},"Un test du stub exprime directement le contrat de déterminisme :",[46,653,655],{"className":63,"code":654,"language":65,"meta":52,"style":52},"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",[24,656,657,667,693,715,720,725],{"__ignoreMap":52},[69,658,659,661,664],{"class":71,"line":72},[69,660,487],{"class":75},[69,662,663],{"class":79}," test_the_same_job_twice_is_byte_identical",[69,665,666],{"class":83},"(tmp_path, profile):\n",[69,668,669,672,674,677,680,682,685,688,691],{"class":71,"line":94},[69,670,671],{"class":83},"    first ",[69,673,130],{"class":75},[69,675,676],{"class":83}," make_manifest(tmp_path, ",[69,678,679],{"class":234},"output",[69,681,130],{"class":75},[69,683,684],{"class":83},"tmp_path ",[69,686,687],{"class":75},"\u002F",[69,689,690],{"class":155}," \"a.png\"",[69,692,225],{"class":83},[69,694,695,698,700,702,704,706,708,710,713],{"class":71,"line":100},[69,696,697],{"class":83},"    second ",[69,699,130],{"class":75},[69,701,676],{"class":83},[69,703,679],{"class":234},[69,705,130],{"class":75},[69,707,684],{"class":83},[69,709,687],{"class":75},[69,711,712],{"class":155}," \"b.png\"",[69,714,225],{"class":83},[69,716,717],{"class":71,"line":112},[69,718,719],{"class":83},"    run(profile, first)\n",[69,721,722],{"class":71,"line":124},[69,723,724],{"class":83},"    run(profile, second)\n",[69,726,727,730,733,735],{"class":71,"line":146},[69,728,729],{"class":75},"    assert",[69,731,732],{"class":83}," first.output.read_bytes() ",[69,734,504],{"class":75},[69,736,737],{"class":83}," second.output.read_bytes()\n",[17,739,740],{},"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.",[12,742,744],{"id":743},"_6-ce-que-seul-le-moteur-réel-peut-confirmer","6. Ce que seul le moteur réel peut confirmer",[17,746,747],{},"Un double de test ne peut pas prouver :",[601,749,750,753,756,759,762,765],{},[604,751,752],{},"le chargement effectif du checkpoint et d'un encodeur de remplacement ;",[604,754,755],{},"l'application des LoRA dans le bon ordre ;",[604,757,758],{},"la compatibilité entre l'architecture et les adaptateurs ;",[604,760,761],{},"la qualité de la segmentation sur des images réelles ;",[604,763,764],{},"la consommation de mémoire et le débit ;",[604,766,767],{},"la qualité visuelle du résultat.",[17,769,770],{},"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.",[12,772,774],{"id":773},"_7-une-vérification-reproductible","7. Une vérification reproductible",[17,776,777,778,788,789,792,793,796],{},"La ",[779,780,784,785],"a",{"href":781,"rel":782},"https:\u002F\u002Fgithub.com\u002Fstratorys\u002Fmlx-diffusion-lab\u002Ftree\u002F4720537",[783],"nofollow","révision ",[24,786,787],{},"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 ",[24,790,791],{},"dev",". Une\nvérification avec ",[24,794,795],{},"importlib.util.find_spec"," y a confirmé l'absence de Torch,\nMFLUX, MLX et Transformers.",[46,798,802],{"className":799,"code":800,"language":801,"meta":52,"style":52},"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",[24,803,804,817,831,835,845,859,864,868,887],{"__ignoreMap":52},[69,805,806,809,811,814],{"class":71,"line":72},[69,807,808],{"class":83},"UV_PROJECT_ENVIRONMENT",[69,810,130],{"class":75},[69,812,813],{"class":155},"\u002Ftmp\u002Fimagegen-dev-only",[69,815,816],{"class":79}," \\\n",[69,818,819,822,825,828],{"class":71,"line":94},[69,820,821],{"class":155},"  uv",[69,823,824],{"class":155}," sync",[69,826,827],{"class":87}," --extra",[69,829,830],{"class":155}," dev\n",[69,832,833],{"class":71,"line":100},[69,834,163],{"emptyLinePlaceholder":162},[69,836,837,839,841,843],{"class":71,"line":112},[69,838,808],{"class":83},[69,840,130],{"class":75},[69,842,813],{"class":155},[69,844,816],{"class":79},[69,846,847,849,851,854,857],{"class":71,"line":124},[69,848,821],{"class":155},[69,850,171],{"class":155},[69,852,853],{"class":155}," python",[69,855,856],{"class":87}," -c",[69,858,816],{"class":87},[69,860,861],{"class":71,"line":146},[69,862,863],{"class":155},"  \"import importlib.util; print({n: importlib.util.find_spec(n) is not None for n in ('torch', 'mflux', 'mlx', 'transformers')})\"\n",[69,865,866],{"class":71,"line":152},[69,867,163],{"emptyLinePlaceholder":162},[69,869,870,872,874,876,879,881,884],{"class":71,"line":159},[69,871,808],{"class":83},[69,873,130],{"class":75},[69,875,813],{"class":155},[69,877,878],{"class":79}," uv",[69,880,171],{"class":155},[69,882,883],{"class":155}," pytest",[69,885,886],{"class":87}," -q\n",[69,888,889,891,893,895,897,899,902,905,908,911],{"class":71,"line":166},[69,890,808],{"class":83},[69,892,130],{"class":75},[69,894,813],{"class":155},[69,896,878],{"class":79},[69,898,171],{"class":155},[69,900,901],{"class":155}," ruff",[69,903,904],{"class":155}," check",[69,906,907],{"class":155}," src",[69,909,910],{"class":155}," tests",[69,912,913],{"class":155}," bench\n",[17,915,916],{},"Le 14 septembre 2026, cette procédure a produit le résultat suivant :",[46,918,921],{"className":919,"code":920,"language":51,"meta":52},[49],"{'torch': False, 'mflux': False, 'mlx': False, 'transformers': False}\n144 passed, 2 warnings in 2.97s\nAll checks passed!\n",[24,922,920],{"__ignoreMap":52},[17,924,925],{},"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.",[17,927,928],{},"Un smoke test réel minimal utilise un environnement distinct :",[46,930,932],{"className":799,"code":931,"language":801,"meta":52,"style":52},"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",[24,933,934,945,961,965,975,989,999,1009,1019],{"__ignoreMap":52},[69,935,936,938,940,943],{"class":71,"line":72},[69,937,808],{"class":83},[69,939,130],{"class":75},[69,941,942],{"class":155},"\u002Ftmp\u002Fimagegen-mflux-smoke",[69,944,816],{"class":79},[69,946,947,949,951,953,956,958],{"class":71,"line":94},[69,948,821],{"class":155},[69,950,824],{"class":155},[69,952,827],{"class":87},[69,954,955],{"class":155}," dev",[69,957,827],{"class":87},[69,959,960],{"class":155}," mflux\n",[69,962,963],{"class":71,"line":100},[69,964,163],{"emptyLinePlaceholder":162},[69,966,967,969,971,973],{"class":71,"line":112},[69,968,808],{"class":83},[69,970,130],{"class":75},[69,972,942],{"class":155},[69,974,816],{"class":79},[69,976,977,979,981,984,987],{"class":71,"line":124},[69,978,821],{"class":155},[69,980,171],{"class":155},[69,982,983],{"class":155}," imagegen-sweep",[69,985,986],{"class":155}," sweeps\u002Fexample.json",[69,988,816],{"class":87},[69,990,991,994,997],{"class":71,"line":146},[69,992,993],{"class":87},"    --profile",[69,995,996],{"class":155}," plain-4b",[69,998,816],{"class":87},[69,1000,1001,1004,1007],{"class":71,"line":152},[69,1002,1003],{"class":87},"    --runner",[69,1005,1006],{"class":155}," mflux",[69,1008,816],{"class":87},[69,1010,1011,1014,1017],{"class":71,"line":159},[69,1012,1013],{"class":87},"    --limit",[69,1015,1016],{"class":87}," 1",[69,1018,816],{"class":87},[69,1020,1021,1024],{"class":71,"line":166},[69,1022,1023],{"class":87},"    --out",[69,1025,1026],{"class":155}," \u002Ftmp\u002Fimagegen-real-smoke-20260914\n",[17,1028,1029,1030,1033],{},"Sur une machine macOS arm64 avec MFLUX 0.19.1 et MLX 0.32.2, le runner a chargé\nle checkpoint public ",[24,1031,1032],{},"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 :",[46,1035,1038],{"className":1036,"code":1037,"language":51,"meta":52},[49],"runner mflux ready in 3.5s\n[1\u002F1] harbour seed 1  harbour_45b9d36c0e_seed1.png  24.65s\n1 produced, 0 failed\n",[24,1039,1037],{"__ignoreMap":52},[17,1041,1042],{},[1043,1044],"img",{"alt":1045,"src":1046},"Port de pierre généré par le smoke test MFLUX","\u002Fassets\u002Fblog\u002Ftester-inference-sans-gpu\u002Fharbour-smoke-test.png",[17,1048,1049],{},[1050,1051,1052,1053,1056],"em",{},"Sortie brute du smoke test, sans retouche ni sélection parmi plusieurs\nvariantes. Le sweep planifiait sept images, mais ",[24,1054,1055],{},"--limit 1"," n'en a exécuté\nqu'une.",[17,1058,1059],{},"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.",[1061,1062],"json-artifact",{"copied-label":1063,"copy-label":1064,"error-label":1065,"file":1066,"title":1067},"Copié","Copier","Le manifeste JSON n’a pas pu être chargé.","tester-inference-sans-gpu\u002Fharbour-smoke-test.manifest.json","Manifeste résolu",[1061,1069],{"copied-label":1063,"copy-label":1064,"error-label":1070,"file":1071,"title":1072},"Le sidecar JSON n’a pas pu être chargé.","tester-inference-sans-gpu\u002Fharbour-smoke-test.metadata.json","Sidecar complet",[17,1074,1075,1076,1079,1080,1083],{},"Le sidecar confirme le runner ",[24,1077,1078],{},"mflux",", le profil ",[24,1081,1082],{},"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.",[12,1085,1087],{"id":1086},"_8-ajouter-un-autre-moteur","8. Ajouter un autre moteur",[17,1089,1090,1091,1094],{},"Un nouveau backend doit charger ses ressources et implémenter ",[24,1092,1093],{},"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.",[17,1096,1097],{},"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.",[12,1099,1101],{"id":1100},"_9-conclusion","9. Conclusion",[17,1103,1104],{},"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.",[17,1106,1107],{},"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.",[1109,1110,1111],"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":52,"searchDepth":94,"depth":94,"links":1113},[1114,1115,1116,1117,1118,1119,1120,1121,1122,1123],{"id":14,"depth":94,"text":15},{"id":30,"depth":94,"text":31},{"id":40,"depth":94,"text":41},{"id":288,"depth":94,"text":289},{"id":473,"depth":94,"text":474},{"id":595,"depth":94,"text":596},{"id":743,"depth":94,"text":744},{"id":773,"depth":94,"text":774},{"id":1086,"depth":94,"text":1087},{"id":1100,"depth":94,"text":1101},"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":6,"description":1127},"reliable-generative-images","6","Construire des pipelines d’images fiables",{"loc":1133,"images":1140},[1141],{"loc":1046},"blog\u002Fisoler-le-moteur-d-inference-pour-tester-sans-gpu",[1144,1145,1146,1147,1148],"software architecture","testing","dependency inversion","inference","deterministic systems","WGoo0ksHPn9VmCN8I3DmNvd5HQ2TSsGO9_c09r3V2Ds",{"id":1151,"title":1152,"author":7,"body":1153,"category":1326,"cover":1125,"date":1327,"description":1328,"extension":1128,"featured":1129,"kind":1329,"language":1330,"meta":1331,"modified":1125,"navigation":162,"path":1332,"published":162,"readingTime":1333,"seo":1334,"seriesKey":1125,"seriesOrder":1125,"seriesTitle":1125,"sitemap":1335,"stem":1336,"tags":1337,"translationKey":1125,"__hash__":1348},"blog\u002Fblog\u002Fchoosing-a-data-query-engine-is-not-a-speed-contest.md","Choosing a data query engine is not a speed contest",{"type":9,"value":1154,"toc":1319},[1155,1158,1161,1164,1167,1170,1173,1176,1179,1182,1185,1188,1192,1195,1198,1201,1204,1207,1210,1213,1216,1219,1222,1225,1228,1232,1235,1238,1241,1244,1247,1250,1254,1257,1260,1263,1266,1269,1273,1276,1279,1282,1285,1288,1291,1295,1298,1301,1304,1307,1310,1313,1316],[17,1156,1157],{},"At some point, every data team ends up in the same room having the same conversation.",[17,1159,1160],{},"Someone asks, \"Which database should we use?\"",[17,1162,1163],{},"Then the familiar answers arrive quickly.",[17,1165,1166],{},"\"We used ClickHouse at my last company.\"",[17,1168,1169],{},"\"DuckDB is supposed to be really fast.\"",[17,1171,1172],{},"\"We already have Postgres running.\"",[17,1174,1175],{},"None of these are bad observations. In fact, they are often useful starting points. But they are not decisions. They are preferences, memories, and conveniences dressed up as conclusions.",[17,1177,1178],{},"Choosing a data query engine is one of the most consequential decisions a data team makes because it shapes how data is stored, queried, operated, scaled, and eventually replaced. The mistake is treating the choice like a search for the fastest engine.",[17,1180,1181],{},"There is no fastest engine.",[17,1183,1184],{},"There is only the engine that fits a particular workload, team, and set of constraints.",[17,1186,1187],{},"The real job is not to pick the best tool in the abstract. It is to make a choice that can still be explained a year later, when the data has grown, the team has changed, and someone inevitably asks, \"Why did we choose this?\"",[12,1189,1191],{"id":1190},"start-with-the-workload","Start With the Workload",[17,1193,1194],{},"Before naming a single technology, it helps to slow down and describe the workload in plain language.",[17,1196,1197],{},"What kind of questions will the system answer?",[17,1199,1200],{},"Are users looking up one record at a time, or are they scanning millions or billions of rows to build reports and dashboards? A system designed for point lookups is very different from one designed for large analytical scans.",[17,1202,1203],{},"How will data arrive?",[17,1205,1206],{},"Some workloads depend on frequent small writes. Others ingest data in large batches. Some data is append-only. Other data needs regular updates and deletes. This matters because mutability is where many engines start to reveal their limits.",[17,1208,1209],{},"What does \"fast\" actually mean?",[17,1211,1212],{},"For an API, fast might mean 10 milliseconds. For an internal dashboard, it might mean two seconds. For an overnight report, it might mean 10 minutes. Without a latency budget, \"fast\" is just a vague hope.",[17,1214,1215],{},"How many users or requests will the system handle at once?",[17,1217,1218],{},"Ten analysts running heavy queries is not the same problem as thousands of application requests per second. Engines that are excellent for one case often make tradeoffs that hurt the other.",[17,1220,1221],{},"And finally, how large is the data today, and how quickly is it growing?",[17,1223,1224],{},"Current size matters, but the growth curve matters more. A comfortable choice at one terabyte can become painful at one hundred.",[17,1226,1227],{},"Once these questions are written down, the list of possible engines usually gets smaller very quickly.",[12,1229,1231],{"id":1230},"understand-the-shape-of-the-tools","Understand the Shape of the Tools",[17,1233,1234],{},"Most query engines have a natural center of gravity.",[17,1236,1237],{},"Columnar OLAP engines, such as ClickHouse, are built for analytical workloads. They store data by column, which makes wide scans and aggregations much cheaper because the engine only reads the columns required by the query. If the common question is \"group, filter, and aggregate a huge number of rows,\" this family is often a strong candidate.",[17,1239,1240],{},"But that strength comes with tradeoffs. High-frequency single-row updates, transactional behavior, and highly mutable data are usually not where these systems feel most natural.",[17,1242,1243],{},"Embedded and in-process engines, such as DataFusion or DuckDB depending on the architecture, solve a different problem. They can bring analytical query power directly into an application or workflow without operating a separate database cluster. This can reduce operational overhead and give the team more control, especially when the query engine is part of a larger system.",[17,1245,1246],{},"Search engines and key-value stores live in another world entirely. If the workload is full-text search, relevance ranking, or very high-rate point lookups, a columnar analytics database is usually the wrong shape. The right tool may look nothing like a traditional analytics engine.",[17,1248,1249],{},"This is why the workload has to come first. The engine is only \"good\" in relation to the problem it is being asked to solve.",[12,1251,1253],{"id":1252},"benchmark-with-your-own-data","Benchmark With Your Own Data",[17,1255,1256],{},"Published benchmarks are tempting because they appear objective. They have charts, numbers, and rankings. But they are usually measuring someone else’s data, someone else’s queries, and someone else’s hardware.",[17,1258,1259],{},"That does not make them useless, but it does make them incomplete.",[17,1261,1262],{},"A useful benchmark is less glamorous. Take a representative slice of your own data. Replay the real queries your users or applications will run. Test the concurrency you expect. Measure the numbers that actually matter: p95 latency, ingestion throughput, cost per query, memory usage, operational complexity, and tail behavior under load.",[17,1264,1265],{},"Most importantly, run the test on infrastructure you would actually be willing to pay for.",[17,1267,1268],{},"The goal is not to produce the most impressive number. The goal is to produce a decision that can be defended.",[12,1270,1272],{"id":1271},"count-the-costs-that-do-not-show-up-in-the-benchmark","Count the Costs That Do Not Show Up in the Benchmark",[17,1274,1275],{},"Raw query speed is easy to compare. It is also rarely the whole story.",[17,1277,1278],{},"The hidden costs often matter more.",[17,1280,1281],{},"Who will operate the system when something breaks at 3am? A database that is 20% faster but requires a dedicated team may be the more expensive choice in practice.",[17,1283,1284],{},"Does the engine have a mature ecosystem? Are there stable drivers, client libraries, monitoring tools, deployment patterns, and people who already know how to run it? Boring and well supported often beats clever and obscure.",[17,1286,1287],{},"How easy is it to leave?",[17,1289,1290],{},"This question is uncomfortable, but important. Every database choice creates some amount of lock-in. Using open formats such as Parquet and Arrow can keep future options open. A good architecture should make it possible to change your mind later without rewriting the entire system.",[12,1292,1294],{"id":1293},"write-the-decision-down","Write the Decision Down",[17,1296,1297],{},"The final output of this process should not only be a database deployment. It should be a short decision record.",[17,1299,1300],{},"Write down the workload you were solving for. Write down the candidates you considered. Write down what you benchmarked, what you measured, what you chose, and what tradeoffs you accepted.",[17,1302,1303],{},"Then write the most important line: when should this decision be revisited?",[17,1305,1306],{},"Maybe the answer changes when the dataset grows by 10x. Maybe it changes when concurrency reaches a certain threshold. Maybe it changes when updates become more frequent, or when operational cost crosses a limit.",[17,1308,1309],{},"A decision with a revisit trigger is healthier than a decision pretending to be permanent.",[17,1311,1312],{},"In the end, choosing a data query engine is not about winning an argument over which technology is best. It is about matching a tool to a workload, making the tradeoffs explicit, and leaving behind reasoning that others can understand.",[17,1314,1315],{},"That is what good technical decision-making looks like.",[17,1317,1318],{},"And in a data team, that reasoning is part of the system too.",{"title":52,"searchDepth":94,"depth":94,"links":1320},[1321,1322,1323,1324,1325],{"id":1190,"depth":94,"text":1191},{"id":1230,"depth":94,"text":1231},{"id":1252,"depth":94,"text":1253},{"id":1271,"depth":94,"text":1272},{"id":1293,"depth":94,"text":1294},"Data Engineering","2026-06-15","A practical guide to choosing the right data query engine by starting with workload, benchmarks, operational costs, and long-term tradeoffs.","concept","en",{},"\u002Fblog\u002Fchoosing-a-data-query-engine-is-not-a-speed-contest","6 min read",{"title":1152,"description":1328},{"loc":1332},"blog\u002Fchoosing-a-data-query-engine-is-not-a-speed-contest",[1338,1339,1340,1341,1342,1343,1344,1345,1346,1347],"data query engines","databases","analytics databases","OLAP","benchmarking","ClickHouse","DuckDB","DataFusion","data architecture","technical decision-making","-vGKLQMHQxuGw-Tp_sybjO2BjFc7jCOD1Cxo3GtsFwY",1792986661567]