[{"data":1,"prerenderedAt":1139},["ShallowReactive",2],{"blog-\u002Fblog\u002Fisolating-the-inference-engine-to-test-without-a-gpu\u002F":3,"blog-translation-\u002Fblog\u002Fisolating-the-inference-engine-to-test-without-a-gpu\u002F":1113,"blog-series-\u002Fblog\u002Fisolating-the-inference-engine-to-test-without-a-gpu\u002F":1128,"i-simple-icons:github":1129,"i-simple-icons:linkedin":1133,"i-simple-icons:bluesky":1135,"i-simple-icons:x":1137},{"id":4,"title":5,"author":6,"body":7,"cover":1105,"date":1106,"description":1107,"extension":1108,"featured":1109,"kind":1110,"language":1111,"meta":1112,"modified":1113,"navigation":147,"path":1114,"published":147,"readingTime":1115,"seo":1116,"seriesKey":1113,"seriesOrder":1113,"seriesTitle":1113,"sitemap":1117,"stem":1120,"tags":1121,"translationKey":1113,"__hash__":1127},"blog\u002Fblog\u002Fisolating-the-inference-engine-to-test-without-a-gpu.md","Isolating the Inference Engine to Test Without a GPU","Lucas Jahier",{"type":8,"value":9,"toc":1094},"minimark",[10,15,19,22,26,29,40,43,46,268,271,275,278,443,446,449,456,460,463,575,578,582,585,634,637,723,726,730,733,753,756,760,781,898,901,907,910,913,1011,1018,1024,1031,1041,1044,1050,1055,1066,1070,1077,1080,1084,1087,1090],[11,12,14],"h2",{"id":13},"_1-the-problem-is-not-just-compute-time","1. The problem is not just compute time",[16,17,18],"p",{},"Request validation, fingerprint computation, queue admission, state\ntransitions, atomic writes, crash recovery and error reporting are ordinary\nsoftware behaviours. None of them should need a diffusion model to be verified.",[16,20,21],{},"If every test loads the weights, the development loop becomes slow, expensive\nand ambiguous. A failure can come from the network, the weights, memory or the\norchestration, without its cause being immediately visible. The solution is to\nisolate the minimal capability expected from the compute: receive a resolved\njob, produce an image and preserve its provenance.",[11,23,25],{"id":24},"_2-two-paths-converge-on-the-same-contract","2. Two paths converge on the same contract",[16,27,28],{},"The HTTP server and the sweep tool do not follow exactly the same path. The API\ncreates durable jobs, puts them in a queue and lets a single worker execute\nthem. The sweep builds a list of manifests directly and passes them to the\nrunner after skipping the outputs that already exist.",[30,31,37],"pre",{"className":32,"code":34,"language":35,"meta":36},[33],"language-text","HTTP request                    sweep file\n     ↓                               ↓\nvalidation, job, queue         validation, planning\n     ↓                               ↓\n   worker                       sweep executor\n     └──────────── manifest ───────────┘\n                         ↓\n                  BaseRunner.run()\n                    ↙         ↘\n                  stub          MFLUX\n","text","",[38,39,34],"code",{"__ignoreMap":36},[16,41,42],{},"The manifest contains the job type, the profile, the prompt, the seed, the\ndimensions, the parameters and, depending on the case, a parent image or the\nmask settings. The runner chooses neither the next identifier, nor the\nresumption policy, nor the output path: those decisions have already been made\nwhen it receives the manifest.",[16,44,45],{},"The shared class handles dispatch, measurement, writing the PNG and the\nsidecar. A backend only implements the generation operation:",[30,47,51],{"className":48,"code":49,"language":50,"meta":36,"style":36},"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        \"\"\"Produce a PIL image.\"\"\"\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",[38,52,53,77,83,95,107,129,135,142,149,160,171,185,211,228,241,247,256,262],{"__ignoreMap":36},[54,55,58,62,66,70,74],"span",{"class":56,"line":57},"line",1,[54,59,61],{"class":60},"snl16","class",[54,63,65],{"class":64},"svObZ"," BaseRunner",[54,67,69],{"class":68},"s95oV","(",[54,71,73],{"class":72},"sDLfK","ABC",[54,75,76],{"class":68},"):\n",[54,78,80],{"class":56,"line":79},2,[54,81,82],{"class":64},"    @abstractmethod\n",[54,84,86,89,92],{"class":56,"line":85},3,[54,87,88],{"class":60},"    def",[54,90,91],{"class":64}," generate",[54,93,94],{"class":68},"(\n",[54,96,98,101,104],{"class":56,"line":97},4,[54,99,100],{"class":68},"        self, ",[54,102,103],{"class":60},"*",[54,105,106],{"class":68},", prompt, seed, steps, guidance, width, height,\n",[54,108,110,113,116,119,122,124,126],{"class":56,"line":109},5,[54,111,112],{"class":68},"        image_path",[54,114,115],{"class":60},"=",[54,117,118],{"class":72},"None",[54,120,121],{"class":68},", image_strength",[54,123,115],{"class":60},[54,125,118],{"class":72},[54,127,128],{"class":68},",\n",[54,130,132],{"class":56,"line":131},6,[54,133,134],{"class":68},"    ) -> Image.Image:\n",[54,136,138],{"class":56,"line":137},7,[54,139,141],{"class":140},"sU2Wk","        \"\"\"Produce a PIL image.\"\"\"\n",[54,143,145],{"class":56,"line":144},8,[54,146,148],{"emptyLinePlaceholder":147},true,"\n",[54,150,152,154,157],{"class":56,"line":151},9,[54,153,88],{"class":60},[54,155,156],{"class":64}," run",[54,158,159],{"class":68},"(self, manifest: Manifest) -> Result:\n",[54,161,163,166,168],{"class":56,"line":162},10,[54,164,165],{"class":68},"        started ",[54,167,115],{"class":60},[54,169,170],{"class":68}," time.perf_counter()\n",[54,172,174,177,179,182],{"class":56,"line":173},11,[54,175,176],{"class":68},"        image, metrics ",[54,178,115],{"class":60},[54,180,181],{"class":72}," self",[54,183,184],{"class":68},".render(manifest)\n",[54,186,188,191,193,196,199,202,205,208],{"class":56,"line":187},12,[54,189,190],{"class":68},"        duration ",[54,192,115],{"class":60},[54,194,195],{"class":72}," round",[54,197,198],{"class":68},"(time.perf_counter() ",[54,200,201],{"class":60},"-",[54,203,204],{"class":68}," started, ",[54,206,207],{"class":72},"3",[54,209,210],{"class":68},")\n",[54,212,214,217,221,223,226],{"class":56,"line":213},13,[54,215,216],{"class":68},"        atomic_image(Path(manifest.output), image, ",[54,218,220],{"class":219},"s9osk","format",[54,222,115],{"class":60},[54,224,225],{"class":140},"\"PNG\"",[54,227,210],{"class":68},[54,229,231,234,236,238],{"class":56,"line":230},14,[54,232,233],{"class":68},"        sidecar ",[54,235,115],{"class":60},[54,237,181],{"class":72},[54,239,240],{"class":68},".sidecar(manifest, metrics, duration)\n",[54,242,244],{"class":56,"line":243},15,[54,245,246],{"class":68},"        atomic_json(Path(manifest.metadata), sidecar)\n",[54,248,250,253],{"class":56,"line":249},16,[54,251,252],{"class":60},"        return",[54,254,255],{"class":68}," Result(\n",[54,257,259],{"class":56,"line":258},17,[54,260,261],{"class":68},"            Path(manifest.output), Path(manifest.metadata), duration, metrics\n",[54,263,265],{"class":56,"line":264},18,[54,266,267],{"class":68},"        )\n",[16,269,270],{},"The real code also measures the duration and checks that the manifest's profile\nis the one served by the process. The important point lies elsewhere: the\nshared writing and provenance logic cannot diverge between the stub and MFLUX.",[11,272,274],{"id":273},"_3-a-stub-engine-that-produces-information","3. A stub engine that produces information",[16,276,277],{},"A useful stub does not return an empty constant. It builds a gradient from the\nprompt, the seed, the dimensions and the generation parameters, then adds a\nreadable caption. For an edit, it also blends the parent image into the result.\nA stub contact sheet therefore remains usable to inspect a sweep and its\nlineage.",[30,279,281],{"className":48,"code":280,"language":50,"meta":36,"style":36},"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",[38,282,283,355,365,375,379,399,414,428],{"__ignoreMap":36},[54,284,285,288,290,293,296,299,302,305,308,310,313,315,317,319,322,324,326,328,331,333,335,337,340,342,345,347,350,352],{"class":56,"line":57},[54,286,287],{"class":68},"material ",[54,289,115],{"class":60},[54,291,292],{"class":60}," f",[54,294,295],{"class":140},"\"",[54,297,298],{"class":72},"{",[54,300,301],{"class":68},"prompt",[54,303,304],{"class":72},"}",[54,306,307],{"class":140},"|",[54,309,298],{"class":72},[54,311,312],{"class":68},"seed",[54,314,304],{"class":72},[54,316,307],{"class":140},[54,318,298],{"class":72},[54,320,321],{"class":68},"steps",[54,323,304],{"class":72},[54,325,307],{"class":140},[54,327,298],{"class":72},[54,329,330],{"class":68},"guidance",[54,332,304],{"class":72},[54,334,307],{"class":140},[54,336,298],{"class":72},[54,338,339],{"class":68},"width",[54,341,304],{"class":72},[54,343,344],{"class":140},"x",[54,346,298],{"class":72},[54,348,349],{"class":68},"height",[54,351,304],{"class":72},[54,353,354],{"class":140},"\"\n",[54,356,357,360,362],{"class":56,"line":79},[54,358,359],{"class":68},"rng ",[54,361,115],{"class":60},[54,363,364],{"class":68}," np.random.default_rng(_stable_seed(material))\n",[54,366,367,370,372],{"class":56,"line":85},[54,368,369],{"class":68},"image ",[54,371,115],{"class":60},[54,373,374],{"class":68}," _gradient(width, height, rng)\n",[54,376,377],{"class":56,"line":97},[54,378,148],{"emptyLinePlaceholder":147},[54,380,381,384,387,390,393,396],{"class":56,"line":109},[54,382,383],{"class":60},"if",[54,385,386],{"class":68}," image_path ",[54,388,389],{"class":60},"is",[54,391,392],{"class":60}," not",[54,394,395],{"class":72}," None",[54,397,398],{"class":68},":\n",[54,400,401,404,406,409,412],{"class":56,"line":131},[54,402,403],{"class":68},"    source ",[54,405,115],{"class":60},[54,407,408],{"class":68}," Image.open(image_path).convert(",[54,410,411],{"class":140},"\"RGB\"",[54,413,210],{"class":68},[54,415,416,418,420,423,426],{"class":56,"line":137},[54,417,403],{"class":68},[54,419,115],{"class":60},[54,421,422],{"class":68}," source.resize((width, height), Image.Resampling.",[54,424,425],{"class":72},"LANCZOS",[54,427,210],{"class":68},[54,429,430,433,435,438,441],{"class":56,"line":144},[54,431,432],{"class":68},"    image ",[54,434,115],{"class":60},[54,436,437],{"class":68}," Image.blend(source, image, ",[54,439,440],{"class":72},"0.45",[54,442,210],{"class":68},[16,444,445],{},"Two identical runs produce the same PNG byte for byte, and two different seeds\nproduce different images. This identity does not extend to the sidecar: its\ntimestamp and duration naturally change from one run to the next.",[16,447,448],{},"The runner's name is part of the fingerprint. A placeholder created by the stub\ntherefore cannot take the name or the cache entry of a future MFLUX output.",[16,450,451,452,455],{},"The stub also has deliberate limits. It does not reproduce the real engine's\nsensitivity to every parameter, ",[38,453,454],{},"image_strength"," in particular, and its\ndeterminism says nothing about MFLUX's determinism across machines. It verifies\nthe behaviour of the orchestration, not the physics of the model.",[11,457,459],{"id":458},"_4-do-not-load-what-was-not-asked-for","4. Do not load what was not asked for",[16,461,462],{},"The decoupling would be incomplete if importing the package still loaded MLX,\nMFLUX or Torch. The heavy dependencies are therefore optional extras, and the\nreal runner is only imported when it is explicitly selected:",[30,464,466],{"className":48,"code":465,"language":50,"meta":36,"style":36},"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",[38,467,468,479,495,509,516,529,541,548],{"__ignoreMap":36},[54,469,470,473,476],{"class":56,"line":57},[54,471,472],{"class":60},"def",[54,474,475],{"class":64}," build_runner",[54,477,478],{"class":68},"(profile, name):\n",[54,480,481,484,487,490,493],{"class":56,"line":79},[54,482,483],{"class":60},"    if",[54,485,486],{"class":68}," name ",[54,488,489],{"class":60},"==",[54,491,492],{"class":140}," \"stub\"",[54,494,398],{"class":68},[54,496,497,500,503,506],{"class":56,"line":85},[54,498,499],{"class":60},"        from",[54,501,502],{"class":68}," imagegen.runners.stub ",[54,504,505],{"class":60},"import",[54,507,508],{"class":68}," StubRunner\n",[54,510,511,513],{"class":56,"line":97},[54,512,252],{"class":60},[54,514,515],{"class":68}," StubRunner(profile)\n",[54,517,518,520,522,524,527],{"class":56,"line":109},[54,519,483],{"class":60},[54,521,486],{"class":68},[54,523,489],{"class":60},[54,525,526],{"class":140}," \"mflux\"",[54,528,398],{"class":68},[54,530,531,533,536,538],{"class":56,"line":131},[54,532,499],{"class":60},[54,534,535],{"class":68}," imagegen.runners.mflux ",[54,537,505],{"class":60},[54,539,540],{"class":68}," MfluxRunner\n",[54,542,543,545],{"class":56,"line":137},[54,544,252],{"class":60},[54,546,547],{"class":68}," MfluxRunner(profile)\n",[54,549,550,553,556,558,561,564,566,569,571,573],{"class":56,"line":144},[54,551,552],{"class":60},"    raise",[54,554,555],{"class":72}," ValueError",[54,557,69],{"class":68},[54,559,560],{"class":60},"f",[54,562,563],{"class":140},"\"unknown runner ",[54,565,298],{"class":72},[54,567,568],{"class":68},"name",[54,570,304],{"class":72},[54,572,295],{"class":140},[54,574,210],{"class":68},[16,576,577],{},"The same principle applies to segmentation. The segmenter is only built on the\nfirst masked job. Tests use a stub geometric segmenter; CLIPSeg, Transformers\nand Torch are not needed to test crops, deterministic noise and recomposition.",[11,579,581],{"id":580},"_5-what-becomes-testable-without-a-model","5. What becomes testable without a model",[16,583,584],{},"This separation covers, among other things:",[586,587,588,592,595,598,601,622,625,628,631],"ul",{},[589,590,591],"li",{},"strict manifest validation;",[589,593,594],{},"sweep planning and resumption;",[589,596,597],{},"deduplication by fingerprint;",[589,599,600],{},"HTTP responses and queue depth;",[589,602,603,604,607,608,607,611,607,614,617,618,621],{},"the ",[38,605,606],{},"queued",", ",[38,609,610],{},"running",[38,612,613],{},"done",[38,615,616],{},"failed"," and ",[38,619,620],{},"interrupted"," statuses;",[589,623,624],{},"the append-only journal and its replay after a restart;",[589,626,627],{},"contact sheet generation;",[589,629,630],{},"crops, stub segmentation and recomposition;",[589,632,633],{},"the invariant that requires a zero delta outside the mask.",[16,635,636],{},"A stub test expresses the determinism contract directly:",[30,638,640],{"className":48,"code":639,"language":50,"meta":36,"style":36},"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",[38,641,642,652,678,700,705,710],{"__ignoreMap":36},[54,643,644,646,649],{"class":56,"line":57},[54,645,472],{"class":60},[54,647,648],{"class":64}," test_the_same_job_twice_is_byte_identical",[54,650,651],{"class":68},"(tmp_path, profile):\n",[54,653,654,657,659,662,665,667,670,673,676],{"class":56,"line":79},[54,655,656],{"class":68},"    first ",[54,658,115],{"class":60},[54,660,661],{"class":68}," make_manifest(tmp_path, ",[54,663,664],{"class":219},"output",[54,666,115],{"class":60},[54,668,669],{"class":68},"tmp_path ",[54,671,672],{"class":60},"\u002F",[54,674,675],{"class":140}," \"a.png\"",[54,677,210],{"class":68},[54,679,680,683,685,687,689,691,693,695,698],{"class":56,"line":85},[54,681,682],{"class":68},"    second ",[54,684,115],{"class":60},[54,686,661],{"class":68},[54,688,664],{"class":219},[54,690,115],{"class":60},[54,692,669],{"class":68},[54,694,672],{"class":60},[54,696,697],{"class":140}," \"b.png\"",[54,699,210],{"class":68},[54,701,702],{"class":56,"line":97},[54,703,704],{"class":68},"    run(profile, first)\n",[54,706,707],{"class":56,"line":109},[54,708,709],{"class":68},"    run(profile, second)\n",[54,711,712,715,718,720],{"class":56,"line":131},[54,713,714],{"class":60},"    assert",[54,716,717],{"class":68}," first.output.read_bytes() ",[54,719,489],{"class":60},[54,721,722],{"class":68}," second.output.read_bytes()\n",[16,724,725],{},"Each write is atomic on its own: every temporary file is renamed on the same\nfile system once it is complete. A reader therefore never sees a half-written\nPNG or JSON file. This is not, however, a transaction spanning the PNG and its\nsidecar: a crash between the two writes can leave a complete image without its\nmetadata.",[11,727,729],{"id":728},"_6-what-only-the-real-engine-can-confirm","6. What only the real engine can confirm",[16,731,732],{},"A test double cannot prove:",[586,734,735,738,741,744,747,750],{},[589,736,737],{},"that the checkpoint and a replacement encoder actually load;",[589,739,740],{},"that LoRAs are applied in the right order;",[589,742,743],{},"that the architecture and the adapters are compatible;",[589,745,746],{},"the quality of segmentation on real images;",[589,748,749],{},"memory consumption and throughput;",[589,751,752],{},"the visual quality of the result.",[16,754,755],{},"These properties belong to a small suite of hardware smoke tests. A successful\nimage does not replace the software tests; it only validates the real inference\npath used to produce it.",[11,757,759],{"id":758},"_7-a-reproducible-check","7. A reproducible check",[16,761,762,772,773,776,777,780],{},[763,764,768,769],"a",{"href":765,"rel":766},"https:\u002F\u002Fgithub.com\u002Fstratorys\u002Fmlx-diffusion-lab\u002Ftree\u002F4720537",[767],"nofollow","Revision ",[38,770,771],{},"4720537","\nof the repository contains 144 tests. They were run in a fresh Python 3.14.4\nenvironment containing only the standard dependencies and the ",[38,774,775],{},"dev"," extra. A\ncheck with ",[38,778,779],{},"importlib.util.find_spec"," confirmed that Torch, MFLUX, MLX and\nTransformers were absent.",[30,782,786],{"className":783,"code":784,"language":785,"meta":36,"style":36},"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",[38,787,788,801,815,819,829,843,848,852,871],{"__ignoreMap":36},[54,789,790,793,795,798],{"class":56,"line":57},[54,791,792],{"class":68},"UV_PROJECT_ENVIRONMENT",[54,794,115],{"class":60},[54,796,797],{"class":140},"\u002Ftmp\u002Fimagegen-dev-only",[54,799,800],{"class":64}," \\\n",[54,802,803,806,809,812],{"class":56,"line":79},[54,804,805],{"class":140},"  uv",[54,807,808],{"class":140}," sync",[54,810,811],{"class":72}," --extra",[54,813,814],{"class":140}," dev\n",[54,816,817],{"class":56,"line":85},[54,818,148],{"emptyLinePlaceholder":147},[54,820,821,823,825,827],{"class":56,"line":97},[54,822,792],{"class":68},[54,824,115],{"class":60},[54,826,797],{"class":140},[54,828,800],{"class":64},[54,830,831,833,835,838,841],{"class":56,"line":109},[54,832,805],{"class":140},[54,834,156],{"class":140},[54,836,837],{"class":140}," python",[54,839,840],{"class":72}," -c",[54,842,800],{"class":72},[54,844,845],{"class":56,"line":131},[54,846,847],{"class":140},"  \"import importlib.util; print({n: importlib.util.find_spec(n) is not None for n in ('torch', 'mflux', 'mlx', 'transformers')})\"\n",[54,849,850],{"class":56,"line":137},[54,851,148],{"emptyLinePlaceholder":147},[54,853,854,856,858,860,863,865,868],{"class":56,"line":144},[54,855,792],{"class":68},[54,857,115],{"class":60},[54,859,797],{"class":140},[54,861,862],{"class":64}," uv",[54,864,156],{"class":140},[54,866,867],{"class":140}," pytest",[54,869,870],{"class":72}," -q\n",[54,872,873,875,877,879,881,883,886,889,892,895],{"class":56,"line":151},[54,874,792],{"class":68},[54,876,115],{"class":60},[54,878,797],{"class":140},[54,880,862],{"class":64},[54,882,156],{"class":140},[54,884,885],{"class":140}," ruff",[54,887,888],{"class":140}," check",[54,890,891],{"class":140}," src",[54,893,894],{"class":140}," tests",[54,896,897],{"class":140}," bench\n",[16,899,900],{},"On 14 September 2026, this procedure produced the following result:",[30,902,905],{"className":903,"code":904,"language":35,"meta":36},[33],"{'torch': False, 'mflux': False, 'mlx': False, 'transformers': False}\n144 passed, 2 warnings in 2.97s\nAll checks passed!\n",[38,906,904],{"__ignoreMap":36},[16,908,909],{},"The two warnings come from deprecations in the FastAPI and Starlette test\ntooling; they signal neither a skipped test nor a failure. Installing the\ndependencies may require network access, but running this suite does not once\nthe environment is built.",[16,911,912],{},"A minimal real smoke test uses a separate environment:",[30,914,916],{"className":783,"code":915,"language":785,"meta":36,"style":36},"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",[38,917,918,929,945,949,959,973,983,993,1003],{"__ignoreMap":36},[54,919,920,922,924,927],{"class":56,"line":57},[54,921,792],{"class":68},[54,923,115],{"class":60},[54,925,926],{"class":140},"\u002Ftmp\u002Fimagegen-mflux-smoke",[54,928,800],{"class":64},[54,930,931,933,935,937,940,942],{"class":56,"line":79},[54,932,805],{"class":140},[54,934,808],{"class":140},[54,936,811],{"class":72},[54,938,939],{"class":140}," dev",[54,941,811],{"class":72},[54,943,944],{"class":140}," mflux\n",[54,946,947],{"class":56,"line":85},[54,948,148],{"emptyLinePlaceholder":147},[54,950,951,953,955,957],{"class":56,"line":97},[54,952,792],{"class":68},[54,954,115],{"class":60},[54,956,926],{"class":140},[54,958,800],{"class":64},[54,960,961,963,965,968,971],{"class":56,"line":109},[54,962,805],{"class":140},[54,964,156],{"class":140},[54,966,967],{"class":140}," imagegen-sweep",[54,969,970],{"class":140}," sweeps\u002Fexample.json",[54,972,800],{"class":72},[54,974,975,978,981],{"class":56,"line":131},[54,976,977],{"class":72},"    --profile",[54,979,980],{"class":140}," plain-4b",[54,982,800],{"class":72},[54,984,985,988,991],{"class":56,"line":137},[54,986,987],{"class":72},"    --runner",[54,989,990],{"class":140}," mflux",[54,992,800],{"class":72},[54,994,995,998,1001],{"class":56,"line":144},[54,996,997],{"class":72},"    --limit",[54,999,1000],{"class":72}," 1",[54,1002,800],{"class":72},[54,1004,1005,1008],{"class":56,"line":151},[54,1006,1007],{"class":72},"    --out",[54,1009,1010],{"class":140}," \u002Ftmp\u002Fimagegen-real-smoke-20260914\n",[16,1012,1013,1014,1017],{},"On a macOS arm64 machine with MFLUX 0.19.1 and MLX 0.32.2, the runner loaded\nthe public checkpoint ",[38,1015,1016],{},"mlx-community\u002Fflux2-klein-4b-4bit"," in 3.5 seconds. It\nthen produced, in 24.65 seconds, a 768 × 1024 pixel image, its manifest, its\nsidecar and the sweep index:",[30,1019,1022],{"className":1020,"code":1021,"language":35,"meta":36},[33],"runner mflux ready in 3.5s\n[1\u002F1] harbour seed 1  harbour_45b9d36c0e_seed1.png  24.65s\n1 produced, 0 failed\n",[38,1023,1021],{"__ignoreMap":36},[16,1025,1026],{},[1027,1028],"img",{"alt":1029,"src":1030},"Stone harbour generated by the MFLUX smoke test","\u002Fassets\u002Fblog\u002Fisolating-the-inference-engine-to-test-without-a-gpu\u002Fharbour-smoke-test.png",[16,1032,1033],{},[1034,1035,1036,1037,1040],"em",{},"Raw smoke test output, with no retouching and no selection among several\nvariants. The sweep planned seven images, but ",[38,1038,1039],{},"--limit 1"," ran only one.",[16,1042,1043],{},"The manifest preserves the inputs to the computation. The sidecar adds the\neffective identity of the engine and the measurements of the run. Both complete\nartifacts are shown below.",[1045,1046],"json-artifact",{"error-label":1047,"file":1048,"title":1049},"The JSON manifest could not be loaded.","isolating-the-inference-engine-to-test-without-a-gpu\u002Fharbour-smoke-test.manifest.json","Resolved manifest",[1045,1051],{"error-label":1052,"file":1053,"title":1054},"The JSON sidecar could not be loaded.","isolating-the-inference-engine-to-test-without-a-gpu\u002Fharbour-smoke-test.metadata.json","Full sidecar",[16,1056,1057,1058,1061,1062,1065],{},"The sidecar confirms the ",[38,1059,1060],{},"mflux"," runner, the ",[38,1063,1064],{},"plain-4b"," profile, four steps, a\nguidance of 1.0 and no adapter. This smoke test therefore verifies neither\nLoRAs, nor the replacement encoder, nor CLIPSeg: each one requires an\nadditional real test case.",[11,1067,1069],{"id":1068},"_8-adding-another-engine","8. Adding another engine",[16,1071,1072,1073,1076],{},"A new backend must load its resources and implement ",[38,1074,1075],{},"generate(...)"," to return a\nPIL image. Manifest handling, masking, writes and provenance stay in the shared\nclass; the API, the queue and the sweep planner do not have to change.",[16,1078,1079],{},"This promise does, however, come with a condition: the contract must not be\nimplicitly modelled on MFLUX. Adding a second real engine would be the strongest\ntest of its generality. Until then, the stub mainly proves that the\norchestration is not coupled to loading MFLUX.",[11,1081,1083],{"id":1082},"_9-conclusion","9. Conclusion",[16,1085,1086],{},"A narrow boundary between orchestration and inference transforms the\ntestability of a generative platform. The stub makes tests of software\nbehaviour fast and deterministic; the real engine focuses on the hardware and\nvisual properties that no double can simulate.",[16,1088,1089],{},"This separation does not only serve testing. It also prepares for swapping a\nbackend and for running inference in a separate process. Distributing the\ncompute across several machines still requires making transport, capabilities\nand network failures explicit: that is the next step, not a property already\nprovided by the local contract alone.",[1091,1092,1093],"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":36,"searchDepth":79,"depth":79,"links":1095},[1096,1097,1098,1099,1100,1101,1102,1103,1104],{"id":13,"depth":79,"text":14},{"id":24,"depth":79,"text":25},{"id":273,"depth":79,"text":274},{"id":458,"depth":79,"text":459},{"id":580,"depth":79,"text":581},{"id":728,"depth":79,"text":729},{"id":758,"depth":79,"text":759},{"id":1068,"depth":79,"text":1069},{"id":1082,"depth":79,"text":1083},"\u002Fassets\u002Fblog\u002Fisolating-the-inference-engine-to-test-without-a-gpu\u002Fcover.png","2026-09-14","A shared contract and a deterministic stub engine to test orchestration independently of the model weights.","md",false,"research-note","en",{},null,"\u002Fblog\u002Fisolating-the-inference-engine-to-test-without-a-gpu","7 min read",{"title":5,"description":1107},{"loc":1114,"images":1118},[1119],{"loc":1030},"blog\u002Fisolating-the-inference-engine-to-test-without-a-gpu",[1122,1123,1124,1125,1126],"software architecture","testing","dependency inversion","inference","deterministic systems","a5WqD3dke7q8VSi67yChuznVQS58VnxbEp1kTh5UbJQ",[],{"left":1130,"top":1130,"width":1131,"height":1131,"rotate":1130,"vFlip":1109,"hFlip":1109,"body":1132},0,24,"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":1130,"top":1130,"width":1131,"height":1131,"rotate":1130,"vFlip":1109,"hFlip":1109,"body":1134,"hidden":147},"\u003Cpath fill=\"currentColor\" d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z\"\u002F>",{"left":1130,"top":1130,"width":1131,"height":1131,"rotate":1130,"vFlip":1109,"hFlip":1109,"body":1136},"\u003Cpath fill=\"currentColor\" d=\"M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037c-.856 3.061-3.978 3.842-6.755 3.37c4.854.826 6.089 3.562 3.422 6.299c-5.065 5.196-7.28-1.304-7.847-2.97c-.104-.305-.152-.448-.153-.327c0-.121-.05.022-.153.327c-.568 1.666-2.782 8.166-7.847 2.97c-2.667-2.737-1.432-5.473 3.422-6.3c-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026\"\u002F>",{"left":1130,"top":1130,"width":1131,"height":1131,"rotate":1130,"vFlip":1109,"hFlip":1109,"body":1138},"\u003Cpath fill=\"currentColor\" d=\"M14.234 10.162L22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299l-.929-1.329L3.076 1.56h3.182l5.965 8.532l.929 1.329l7.754 11.09h-3.182z\"\u002F>",1793168446816]