<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: phinn markson</title>
    <description>The latest articles on DEV Community by phinn markson (@marsonp).</description>
    <link>https://dev.to/marsonp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3831843%2Fd470420f-bbd2-4a4d-97d9-a8f7b797f76c.jpg</url>
      <title>DEV Community: phinn markson</title>
      <link>https://dev.to/marsonp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marsonp"/>
    <language>en</language>
    <item>
      <title>the finger, the moon and the idle T4 a pre-flight linter for ML environment mismatches</title>
      <dc:creator>phinn markson</dc:creator>
      <pubDate>Sat, 11 Jul 2026 20:06:05 +0000</pubDate>
      <link>https://dev.to/marsonp/the-finger-the-moon-and-the-idle-t4-a-pre-flight-linter-for-ml-environment-mismatches-3jj4</link>
      <guid>https://dev.to/marsonp/the-finger-the-moon-and-the-idle-t4-a-pre-flight-linter-for-ml-environment-mismatches-3jj4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &amp;lt;!What I Built
&lt;/h2&gt;

&lt;p&gt;There's a teaching in the Buddhist tradition about a finger pointing at the moon. I say "Hey, look at the moon!" and point upward — and you, instead, are distracted by my finger, and respond with critique about my finger. As with all spiritual parables, the meanings to be drawn are contextual and personal. Here is mine: I built a small tool called nocando (pip install nocando), and nocando is the finger. The moon is energy allocation.&lt;br&gt;
Let me start with the finger, since that's what the dev challenge asks for.&lt;br&gt;
nocando is a pre-flight environment-semantics linter for ML workloads. It answers one question before you occupy hardware: is the code you wrote coherent with the declared environment? Syntax linters know your language. Type checkers know your data shapes. Nothing knows that from sklearn.svm import SVC plus "Runtime: T4" is an incoherent sentence. Now something does. This has been fomenting, churning and brewing... passion — desire...I spent a year in machine learning chasing my tail through undocumented mismatches: the constant expectation that I should have known something that is never given to me, anywhere, in any linear order. And this week I had enough of this for all of us because I know this is not unique to me, every time I look things up, I am met with some form of "yup... " as if acknowledgement makes the externalization acceptable. It doesn't.  I&lt;br&gt;
sat for three and a half hours on a Colab T4 at 0% utilization because, unbeknownst to me, scikit-learn for SVM is CPU-only. It has no CUDA backend. That fact is thoroughly documented, if you know to look for that. Buried in Stack threads and subreddits, indexed by symptom, findable only after you've already failed, because failure is what generates the search terms. Nothing says this up front. So I built a thing that says it up front. That T4 could have been someone else's, or simply not occupied.&lt;br&gt;
Now the moon.&lt;br&gt;
Notice what that wasted session actually was: not a worse model, not a slower result. Nothing. Heat. Eliminating it asks no one to sacrifice anything — nobody's workflow degrades, nobody is told to want less. Multiply one wasted Tuesday across every student, bootcamp cohort, and tutorial-follower hitting the same mismatch, and you get a large number composed entirely of moments nobody valued. No one watches the intersections. I do. Like a lint trap. &lt;br&gt;
There is no shortage of accepted scholarly literature, across a wide berth of domains, laying out in detail how much resource there actually is — energy, water, compute. Our major malfunction is not one of needing more. It is, simply, allocation. Simple, not easy — that's a recovery slogan, and the implication is exact: recovery is not complicated, but our thinking makes it so. Repent means just this: turn around, turn back from your thinking. And this is what is not easy — unsticking from a way of thinking things should be. More is better. Better is better. Or: more is simply more, and better might only be relatively so, for a few, in the&lt;br&gt;
short term.&lt;br&gt;
I'll offer one anecdote about how sticky that thinking is. In my first "big data" graduate course, my distance-learning group and I had a divorce. One mitigating factor, in my perception, was an old, uninformed bias about what makes data "big." To them, big data had to meet some &lt;em&gt;n&lt;/em&gt; threshold — never a complexity or quality one. Contemporary definitions freely state that complexity is absolutely a characteristic that can make data big; they were unable to accept this. Quantity was legible to them. Quality was not. Ultimately, we divorced. I share this because that bias is not confined to one group project — it's the same reflex driving the industry's fraught path of more energy needs for more data centers for&lt;br&gt;
more data for more training for more models for more. I propose nothing novel at all. Allocation. We already use allocation in all sorts of systems, and fail to use it in all sorts of others, to our respective advantage and detriment. We do not need more. We simply need better.&lt;br&gt;
A linter cannot fix an industry's thinking. A finger is not the moon.&lt;br&gt;
But a finger that points at the exact line of your file, one second&lt;br&gt;
before you burn the hardware, and says this will produce heat and&lt;br&gt;
nothing else — here's the cheaper allocation — that's a small, tangible, working piece of the turn.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nocando train.py &lt;span class="nt"&gt;--runtime&lt;/span&gt; t4
&lt;span class="go"&gt;
────────────────────────────────────────────────────────────────
 nocando pre-flight audit — train.py
 declared runtime: T4  (accelerator)
────────────────────────────────────────────────────────────────
 VERDICT: ✗ NO CAN DO

  [BLOCK]  scikit-learn (line 11: `clf = GridSearchCV(SVC(...`):
           scikit-learn executes on CPU only. It has no CUDA backend
           and will not touch a GPU regardless of runtime. Heavy
           CPU-bound calls found: SVC, GridSearchCV. These will run
           entirely on CPU while the T4 idles at ~0% utilization.
           ↳ NVIDIA cuML (RAPIDS) is a drop-in GPU replacement, or
             switch to a CPU runtime (often faster AND free).
────────────────────────────────────────────────────────────────
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It reads both directions — here it catches the opposite mismatch, plus an&lt;br&gt;
environment that contradicts itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;$ nocando environment.yml&lt;/span&gt;

  &lt;span class="s"&gt;[advise] contradictory pin&lt;/span&gt;&lt;span class="na"&gt;: numpy (line 6&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;- numpy=1.26.4` and&lt;/span&gt;
           &lt;span class="s"&gt;line 9&lt;/span&gt;&lt;span class="na"&gt;: `- numpy==2.1.0`)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pinned to different major&lt;/span&gt;
           &lt;span class="s"&gt;versions in this artifact. Whichever installs last&lt;/span&gt;
           &lt;span class="s"&gt;silently wins or the resolver fails outright.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And when the deterministic core hits its own honest limits, it can hand&lt;br&gt;
what it learned to a language model — clearly labeled as advisory:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ nocando frankenstein_project.py --runtime t4 --agent

 ...deterministic report first, including:
 unscanned (recognized, no rule exists yet): some_obscure_lab_package
 — absence of a warning above means 'no rule fired,'
   not 'verified compatible.'

════════════════════════════════════════════════════════════════
 ADVISORY (Gemini) — NOT part of the deterministic verdict above.
════════════════════════════════════════════════════════════════
 ...Gemini answers only the open questions the core refused to
 guess at, instructed to mark its guesses as guesses.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Try it yourself in ten seconds — no launch, no deploy, nothing to&lt;br&gt;
configure. &lt;code&gt;pip install nocando&lt;/code&gt;, then point it at any script, notebook,&lt;br&gt;
&lt;code&gt;environment.yml&lt;/code&gt;, or even a PDF assignment handout you already have. It&lt;br&gt;
reads your file, prints a report, exits. Exit codes (0/1/2) drop straight&lt;br&gt;
into CI and pre-commit hooks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/phinnphace" rel="noopener noreferrer"&gt;
        phinnphace
      &lt;/a&gt; / &lt;a href="https://github.com/phinnphace/nocando" rel="noopener noreferrer"&gt;
        nocando
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Nocando is a pre-flight environment-semantics linter for ML workloads. It reads your script, notebook, or — this part matters — the assignment handout itself, and answers one question before you occupy hardware: is the code you wrote coherent with the environment you declared?   
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;nocando&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A pre-flight &lt;strong&gt;environment-semantics linter&lt;/strong&gt; for ML workloads. It answers one
question &lt;em&gt;before&lt;/em&gt; you occupy hardware — in either direction:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is the code you wrote coherent with the environment you declared?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That covers CPU-only workloads parked on GPUs (waste), unconditional
&lt;code&gt;.cuda()&lt;/code&gt; calls headed for a CPU runtime (crash at minute 40), CUDA-requiring
libraries on Apple Silicon (import failure), and wrong-generation hardware
(flash-attn needs compute capability &amp;gt;= 8.0; a Colab T4 is 7.5). The unit of
analysis is the &lt;em&gt;mismatch&lt;/em&gt;. Deliberate slow-on-CPU choices are not flagged —
can't legislate morality.&lt;/p&gt;
&lt;p&gt;Syntax linters know your language. Type checkers know your data shapes
Nothing knows that &lt;code&gt;from sklearn.svm import SVC&lt;/code&gt; plus "Runtime: T4" is an
incoherent sentence — the constraint lives at the intersection of
library × hardware × platform, and nobody owns intersections. &lt;code&gt;nocando&lt;/code&gt; owns
this one. Zero LLM calls, zero telemetry, pure static analysis.&lt;/p&gt;
&lt;p&gt;Certified by Ted (stoop tabby, QA).&lt;/p&gt;
&lt;div class="markdown-heading"&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/phinnphace/nocando" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;PyPI: &lt;a href="https://pypi.org/project/nocando/" rel="noopener noreferrer"&gt;https://pypi.org/project/nocando/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Pure-Python static analysis — deliberately &lt;strong&gt;zero LLM calls, zero&lt;br&gt;
telemetry, stdlib only&lt;/strong&gt; in the core. A pre-flight check you can't trust&lt;br&gt;
deterministically is just another thing to debug. It never executes the&lt;br&gt;
code it audits.&lt;/p&gt;

&lt;p&gt;Three audit dimensions, each born from a real failure of mine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Runtime coherence&lt;/strong&gt; — CPU-only workloads pointed at GPUs;&lt;br&gt;
unconditional &lt;code&gt;.cuda()&lt;/code&gt; calls headed for CPU runtimes (which crash at&lt;br&gt;
minute 40, right after the data prep you already waited through); and&lt;br&gt;
hardware-generation traps like flash-attn on a T4 (needs compute&lt;br&gt;
capability ≥ 8.0; a T4 is 7.5 — a constraint that lives nowhere a&lt;br&gt;
beginner would ever look).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment-manager coherence&lt;/strong&gt; — conda + pip install-order&lt;br&gt;
corruption (conda's own docs bury "conda first, pip last" three&lt;br&gt;
paragraphs deep in a blog post nobody reads until their environment is&lt;br&gt;
already dead), and the notebook &lt;code&gt;!pip&lt;/code&gt; vs &lt;code&gt;%pip&lt;/code&gt; trap, where an&lt;br&gt;
install "succeeds" into a different Python than your kernel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ingestion honesty&lt;/strong&gt; — for Frankenstein code mixing libraries it has&lt;br&gt;
never seen together, nocando explicitly reports every&lt;br&gt;
recognized-but-unscanned library, so silence is never mistaken for&lt;br&gt;
"verified compatible." It flags self-contradictory version pins&lt;br&gt;
because that check is pure logic and can never go stale — the tool&lt;br&gt;
refuses to hardcode a compatibility phonebook that would start&lt;br&gt;
decaying the day it was written.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It reads Python, R (Colab serves both kernels — same trap, different&lt;br&gt;
tongue), notebooks, conda manifests, and prose: feed it the assignment&lt;br&gt;
PDF &lt;em&gt;before&lt;/em&gt; any code exists and it audits the workload the document is&lt;br&gt;
designed to produce. Every finding names its location and quotes the&lt;br&gt;
offending line — &lt;code&gt;(line 11: ...)&lt;/code&gt; for scripts, &lt;code&gt;(cell 3: ...)&lt;/code&gt; for&lt;br&gt;
notebooks, because Colab shows you cells, not line counts.&lt;/p&gt;

&lt;p&gt;49 tests, and the fixtures are real: my actual graduate coursework, fed&lt;br&gt;
through cold. One of those specimens revealed that the tool itself had&lt;br&gt;
shipped with an unexamined assumption — "ML means Python" — until my own&lt;br&gt;
R coursework caught my own linter doing the exact thing it was built to&lt;br&gt;
detect. That finding is preserved in the repo's lineage docs on purpose.&lt;br&gt;
Nobody is exempt from the audit, including the auditor.&lt;/p&gt;

&lt;p&gt;But a deterministic core has a horizon, and the tool is honest about&lt;br&gt;
where it sits: "support vector machine" written in words instead of&lt;br&gt;
&lt;code&gt;SVC()&lt;/code&gt;, an unrecognized lab package nobody has mapped, a training task&lt;br&gt;
described entirely in prose. The core refuses to guess at these — so I&lt;br&gt;
built the layer where guessing is allowed to live, clearly labeled.&lt;br&gt;
&lt;code&gt;nocando --handoff&lt;/code&gt; packages the full report plus its open questions&lt;br&gt;
into a prompt you can paste into any assistant.&lt;code&gt;nocando --agent&lt;/code&gt; goes one step further: it sends that same prompt to Gemini in a single API call and prints the response under an ADVISORY banner — commentary on the report, never part of it. The invariant is tested, not aspirational: neither flag can alter a finding, a verdict, or an exit code. Gemini answers only the questions the deterministic core explicitly could not, and is instructed to mark its guesses as guesses.&lt;br&gt;
That's what I think meaningful AI integration looks like: not a model in the loop, but a model at the boundary — consulted exactly where determinism ends, and not one line before. Nothing is good or bad, it is our relationship with it. Probabilistic AI can be awesome. The right tool for the right job, in the right way. It's like going to the hardware store for bread. Making this tool covers a lot of unknowable ground. It can't be a look up table. Updates, and software versions change every day. The entire computing landscape is constantly in  flux. If this were purely agentic it would be an energy suck. The question is/was; where in here should determinism and agentic forces best interweave so that like, any relationship, they work better together. &lt;br&gt;
Certified by Ted (stoop tabby, QA). --&amp;gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&amp;lt;!nocando's --agent flag sends the&lt;br&gt;
deterministic audit's unresolved questions to Gemini&lt;br&gt;
(google-genai, one generate_content call, model configurable via&lt;br&gt;
GEMINI_MODEL). The integration is deliberately narrow: the&lt;br&gt;
deterministic report is the authority; Gemini is the consultant for the&lt;br&gt;
ambiguities the core refuses to guess at — unscanned libraries,&lt;br&gt;
framework-unconfirmed prose, allocation alternatives. Advisory only,&lt;br&gt;
invariant-tested, degrades gracefully without a key.--&amp;gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
  </channel>
</rss>
