<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://golithe.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://golithe.dev/" rel="alternate" type="text/html" /><updated>2026-11-11T14:07:16+00:00</updated><id>https://golithe.dev/feed.xml</id><title type="html">golithe</title><subtitle>ML engineer working on computer vision and GeoAI. Notes on the engineering that makes models usable.</subtitle><entry><title type="html">Testing a model that outputs polygons: how strict is strict enough?</title><link href="https://golithe.dev/2026/11/11/testing-models-that-output-polygons.html" rel="alternate" type="text/html" title="Testing a model that outputs polygons: how strict is strict enough?" /><published>2026-11-11T00:00:00+00:00</published><updated>2026-11-11T00:00:00+00:00</updated><id>https://golithe.dev/2026/11/11/testing-models-that-output-polygons</id><content type="html" xml:base="https://golithe.dev/2026/11/11/testing-models-that-output-polygons.html"><![CDATA[<p>If you test models that output geometry: how do you set your tolerances?</p>

<p>Here’s my take. One of our models at Dira GeoSystems takes an aerial image and returns polygons. Testing that the service still returns the right polygons after a code change or a library update was more challenging than I thought.</p>

<p>The first idea was a snapshot test: I thought I could just run a few fixed tiles, commit the output, and let tests fail if anything changes. I found out quickly that that doesn’t even work on CPU (yes, CPU, not GPU: our CI runners don’t have GPUs). Floating point details differ between machines, so the same code gives slightly different coordinates on my laptop and on the GitHub runner.</p>

<p>So I realized that the simplest possible test probably would just be to compare polygon by polygon. The number of polygons has to match exactly, and every expected polygon needs its own counterpart with an IoU of at least 0.999. Strict enough to catch a real change, loose enough to survive a different CPU.</p>

<p>On GPU, even that fails. We run inference in mixed precision: on the GPU, most of the math runs in 16-bit floats instead of 32-bit, which is faster but only keeps about three significant digits. Each GPU generation uses its own optimised kernels, which add numbers up in a different order, so the results differ slightly. Same model, same tile: an RTX 5090 and an RTX 6000 Ada gave a merged-area IoU between 0.988 and 0.9995, and one polygon more or less. So depending on the card, you get a different number of polygons.</p>

<p>That’s why the GPU test compares the total area instead: IoU of at least 0.98, and the count may differ by two.</p>

<p>A few more things I ran into:</p>

<ul>
  <li>One fixture is a JPEG-compressed tile. JPEG decoding depends on the GDAL version, so a library update can change the input pixels before the model even sees them.</li>
  <li>The GPU test is expensive (build the image, start a pod, run the tiles), so it doesn’t run on every change (more on that in a future post).</li>
  <li>When the strict CPU test fails after an update, it’s usually a tiny shift. If the benchmark moves by around 0.1 percentage points, I accept it and recapture the fixtures. No big analysis.</li>
</ul>

<p>The fixtures have one more simple but very important job. A lot of the service code is written by AI assistants, and the fixtures are the one thing the assistant is not allowed to change. Only a script regenerates them, and only when I decide the output should change.</p>

<p>If you do it differently, I’d like to hear how.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[If you test models that output geometry: how do you set your tolerances?]]></summary></entry><entry><title type="html">Rollerblades and rocket ships: keeping an early inference pipeline ready for scale</title><link href="https://golithe.dev/2026/09/08/rollerblades-and-rocket-ships.html" rel="alternate" type="text/html" title="Rollerblades and rocket ships: keeping an early inference pipeline ready for scale" /><published>2026-09-08T00:00:00+00:00</published><updated>2026-09-08T00:00:00+00:00</updated><id>https://golithe.dev/2026/09/08/rollerblades-and-rocket-ships</id><content type="html" xml:base="https://golithe.dev/2026/09/08/rollerblades-and-rocket-ships.html"><![CDATA[<p>For the geospatial models I am currently building at Dira GeoSystems, our entire runtime fits on one GPU, fed by a sequential queue. But it is all lined up to serve at scale.</p>

<p>My argument is simple: precisely because this is all very early and nobody runs these at scale yet, the temptation to cut corners is strongest.</p>

<p>Why do I say that?</p>

<p>Because in the last couple of years I built roughly 50 prototypes and the main lesson is that it takes much more time to redesign a rocket ship when you originally blueprinted a pair of rollerblades. Usually the rollerblades were already sold as a rocket ship, gold paint and two bottle rockets duct taped on.</p>

<p>I am still a huge fan of YAGNI. But avoiding premature optimization is not the same as ignoring architectural integrity. You do not need to build every future feature now. But you do want to avoid today’s prototype assumptions that make tomorrow incredibly expensive to change.</p>

<p>Three decisions that keep our inference pipeline clean:</p>

<ul>
  <li><strong>Clients ask for a target, like “buildings”, never a model version.</strong> Internally that maps to an adapter, a checkpoint, and a queue. Swap the model, and the client contract does not move.</li>
  <li><strong>The orchestrator cannot see the models.</strong> It knows tiles, batches, and progress, nothing about neural networks. CI checks read the syntax tree and block model imports from leaking past the adapter layer.</li>
  <li><strong>Loading fails loud on a version mismatch.</strong> Every checkpoint carries its provenance. The adapter checks it against the installed package and refuses incompatible weights, so a silent mismatch never gets blamed on the model.</li>
</ul>

<p>The honest constraint: today the API hands the worker a local file path, so both need the same filesystem. Moving to shared object storage is the main thing standing between us and workers we can distribute freely. But that change sits at the infrastructure boundary. It does not touch the client contract or the core pipeline.</p>

<p>My opinion: good architectural boundaries make scale far less painful. But scale is not a free byproduct of good architecture.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[For the geospatial models I am currently building at Dira GeoSystems, our entire runtime fits on one GPU, fed by a sequential queue. But it is all lined up to serve at scale.]]></summary></entry><entry><title type="html">“No kernel image is available”: catching torch and GPU mismatches before you ship</title><link href="https://golithe.dev/2026/08/24/no-kernel-image-is-available.html" rel="alternate" type="text/html" title="“No kernel image is available”: catching torch and GPU mismatches before you ship" /><published>2026-08-24T00:00:00+00:00</published><updated>2026-08-24T00:00:00+00:00</updated><id>https://golithe.dev/2026/08/24/no-kernel-image-is-available</id><content type="html" xml:base="https://golithe.dev/2026/08/24/no-kernel-image-is-available.html"><![CDATA[<p>If you work with NVIDIA GPUs you know the ecosystem is fiddly: drivers, CUDA toolkits, and then whatever framework you actually use on top, like PyTorch, to run training or inference.</p>

<p>Here’s a situation I hit a lot. I pull in some repo I want to try, maybe from a paper. The authors published and moved on, so the repo is stale and the pinned versions (if they even pinned) are ancient. So I bump torch, numpy, python and see what breaks. You’re stuck guessing at a version combo that works, and the trap is that a wrong guess still builds green.</p>

<p>Because if you’re on a newer card, say a Blackwell 50-series, you can hit something nastier.</p>

<p>You run <code class="language-plaintext highlighter-rouge">torch.cuda.is_available()</code>, it returns <code class="language-plaintext highlighter-rouge">True</code>, everything looks fine. Then you actually run something and the process dies with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>no kernel image is available for execution on the device
</code></pre></div></div>

<p>What’s going on: PyTorch wheels ship precompiled kernels for a fixed set of GPU architectures. If you pin an old torch and run it on a newer architecture (<code class="language-plaintext highlighter-rouge">sm_120</code> for Blackwell), the wheel just doesn’t contain kernels for your card. It still builds. <code class="language-plaintext highlighter-rouge">is_available()</code> still says <code class="language-plaintext highlighter-rouge">True</code>. There’s nothing to actually launch, so every CUDA op fails at runtime. PyTorch only warns about this with a <code class="language-plaintext highlighter-rouge">UserWarning</code>, which is easy to miss in logs. Worst case you don’t find out until the container hits the GPU server and the job errors out.</p>

<p>I contributed <a href="https://github.com/replicate/cog/pull/3104">a PR to cog</a> that catches this early. It’s unglamorous infra work, but the kind that saves time and money: <code class="language-plaintext highlighter-rouge">cog doctor</code> now reads your GPU’s compute capability, figures out the exact torch wheel cog will install, and compares that against a compatibility table. If they don’t line up, you get a clear warning before you build instead of after you’ve shipped.</p>

<p>It doesn’t fix the incompatibility. It just points it out early so you can react to it. For me, working somewhere without a big infra team where a lot of this is still manual, that’s worth a lot.</p>

<p>Hope this helps some fellow ML engineers in the same boat. Thanks to the cog reviewer for a thorough pass.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[If you work with NVIDIA GPUs you know the ecosystem is fiddly: drivers, CUDA toolkits, and then whatever framework you actually use on top, like PyTorch, to run training or inference.]]></summary></entry><entry><title type="html">One Python version for Bazel: pyproject.toml as the single source of truth</title><link href="https://golithe.dev/2026/07/08/pyproject-as-single-source-for-python-in-bazel.html" rel="alternate" type="text/html" title="One Python version for Bazel: pyproject.toml as the single source of truth" /><published>2026-07-08T00:00:00+00:00</published><updated>2026-07-08T00:00:00+00:00</updated><id>https://golithe.dev/2026/07/08/pyproject-as-single-source-for-python-in-bazel</id><content type="html" xml:base="https://golithe.dev/2026/07/08/pyproject-as-single-source-for-python-in-bazel.html"><![CDATA[<p>I have built a lot of ML pipelines. Sometimes with fancy models, mostly with boring models. Model work is the part people post about. But whether any of it survives production depends on something much less shiny.</p>

<p>Reproducibility.</p>

<p>If you work with Python, you know that Python makes reproducibility tough in a lot of cases. When you use Python in Bazel, however, you might know that Bazel is one of the few ecosystems that treats the CPython interpreter itself as a pinned build input rather than whatever is on the host machine. But even there, the Python version typically lives in three places: <code class="language-plaintext highlighter-rouge">pyproject.toml</code>, the toolchain registration, and <code class="language-plaintext highlighter-rouge">pip.parse</code>. Nothing keeps them in sync and you have to handle these three versions manually, which is annoying and leads to errors.</p>

<p>But your days of endless crying are over.</p>

<p>With the next rules_python release, <a href="https://github.com/bazel-contrib/rules_python/pull/3514">my recently merged PR</a> ships. Point the build at your <code class="language-plaintext highlighter-rouge">pyproject.toml</code> and the <code class="language-plaintext highlighter-rouge">requires-python</code> field becomes the single source of truth. The interpreter Bazel fetches, the registered toolchain, and the pip resolution all follow that one file. No need for <code class="language-plaintext highlighter-rouge">python_version</code> in BUILD files or <code class="language-plaintext highlighter-rouge">.python-version</code> files in your project root. A wrong Python version in the build is no longer a state the system can reach.</p>

<p>The details: the toolchain is auto-registered, <code class="language-plaintext highlighter-rouge">pyproject.toml</code> is parsed with a pure-Starlark TOML decoder (reading the version needs no host Python), and the file is watched. Which makes automated upgrades trivial: e.g. Renovate just bumps <code class="language-plaintext highlighter-rouge">requires-python</code>, and that single-line PR upgrades interpreter, toolchain and resolved deps together.</p>

<p>By design it expects exact version pinning (<code class="language-plaintext highlighter-rouge">==X.Y.Z</code>). That fits application repos where you want exact, reproducible and hermetic builds (as opposed to published packages where you might be more lenient with version ranges) and is what makes the automation clean.</p>

<p>I know this all sounds incredibly boring. But in my opinion boring and reliable wins over shiny and unreliable.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I have built a lot of ML pipelines. Sometimes with fancy models, mostly with boring models. Model work is the part people post about. But whether any of it survives production depends on something much less shiny.]]></summary></entry></feed>