<?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: Cairn</title>
    <description>The latest articles on DEV Community by Cairn (cairndev).</description>
    <link>https://dev.to/cairndev</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%2Forganization%2Fprofile_image%2F15623%2F0d2a0d40-3631-451e-a702-4af506a9f8f0.png</url>
      <title>DEV Community: Cairn</title>
      <link>https://dev.to/cairndev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cairndev"/>
    <language>en</language>
    <item>
      <title>A governed fine-tune for $0.86: Qwen3-4B on IFC/BIM, with a person approving every step</title>
      <dc:creator>Vibhu</dc:creator>
      <pubDate>Mon, 26 Oct 2026 22:39:40 +0000</pubDate>
      <link>https://dev.to/cairndev/a-governed-fine-tune-for-086-qwen3-4b-on-ifcbim-with-a-person-approving-every-step-2709</link>
      <guid>https://dev.to/cairndev/a-governed-fine-tune-for-086-qwen3-4b-on-ifcbim-with-a-person-approving-every-step-2709</guid>
      <description>&lt;p&gt;Fine-tuning a model is easy to start. Running it responsibly in a team is the hard part. Usually it's a script on a GPU box: nobody approves the spend, nobody records which data went in, and "is it better than the base model?" is a guess.&lt;/p&gt;

&lt;p&gt;We ran a real fine-tune end to end with &lt;a href="https://github.com/cairndev-sh/cairn" rel="noopener noreferrer"&gt;cairn&lt;/a&gt;, the open-source control plane we're building for exactly this, using only public packages from PyPI. Here is what happened, with the real numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The run in numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://huggingface.co/Qwen/Qwen3-4B-Base" rel="noopener noreferrer"&gt;Qwen3-4B-Base&lt;/a&gt;, 4-bit QLoRA (r 16)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;5,646 training rows of IFC/BIM instructions (from 42,680)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;one NVIDIA T4 (AWS g4dn.xlarge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training&lt;/td&gt;
&lt;td&gt;353 steps, 73 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.86 settled&lt;/strong&gt;, against a &lt;strong&gt;$16.50&lt;/strong&gt; worst-case estimate approved up front&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Test loss&lt;/strong&gt; (348 held-out rows)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.955 → 0.218&lt;/strong&gt; (89% lower)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General ability&lt;/td&gt;
&lt;td&gt;no measurable change on ARC, HellaSwag, WinoGrande, PIQA or MMLU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The adapter, the dataset, the training curve and the raw eval results are public: &lt;a href="https://huggingface.co/cairndev/qwen3-4b-ifc-bim-qlora" rel="noopener noreferrer"&gt;&lt;code&gt;cairndev/qwen3-4b-ifc-bim-qlora&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://huggingface.co/datasets/cairndev/ifc-bim-alpaca-dedup" rel="noopener noreferrer"&gt;&lt;code&gt;cairndev/ifc-bim-alpaca-dedup&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two machines, one rule
&lt;/h2&gt;

&lt;p&gt;A small control box runs the cairn engine. It holds the run record, the approval gates and the budget, and it starts and stops the GPU box. The GPU box runs only the trainer. Nothing listens on it: the engine reaches it over SSH. Datasets, checkpoints and the adapter move between the two through S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A person approves the data, and the data gets checked
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dataset-prep/prepare&lt;/code&gt; paused before reading anything, and showed the source, its pinned Hub commit and the split plan. Only after approval did it download, check and split the data.&lt;/p&gt;

&lt;p&gt;The checks found something worth knowing. &lt;strong&gt;85% of the source rows were near-duplicates&lt;/strong&gt; of each other: the dataset is template-generated, so the same IFC statement appears with only the numbers changed. Near-duplicate removal (MinHash, 0.8) kept 6,295 rows. The split is by template family, so variants of one template never sit on both sides of train and test, and the split's leakage check passed.&lt;/p&gt;

&lt;p&gt;That number changed how we read every result after it. Without the check, we'd have trained on 42,680 rows and believed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The launch refuses before it spends
&lt;/h2&gt;

&lt;p&gt;At launch, before any record existed, cairn checked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data roles:&lt;/strong&gt; each dataset version's role, so a test set can't be passed as training data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holdout:&lt;/strong&gt; the training rows don't overlap the validation or test rows, exactly or near.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The GPU estimate:&lt;/strong&gt; priced from the box's hourly rate × each leg's timeout × its retries, $16.50 worst case, which fit the $25 budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it stopped at &lt;strong&gt;&lt;code&gt;approve-run&lt;/code&gt;&lt;/strong&gt;, with that estimate and the training settings on the card. The GPU box was only started after the approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The box starts, trains, and stops itself
&lt;/h2&gt;

&lt;p&gt;After approval:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;compute/acquire&lt;/code&gt; tagged the instance with a lease, then started it.&lt;/li&gt;
&lt;li&gt;A 5-step smoke run proved the whole path on the box: data from S3, the tokenizer, 4-bit loading, a checkpoint back to S3.&lt;/li&gt;
&lt;li&gt;The real training ran 353 steps. Every 50 steps a checkpoint was mirrored to S3, so a killed process resumes instead of restarting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;compute/release&lt;/code&gt; stopped the box right after training&lt;/strong&gt;, before anyone looked at the results, so it didn't bill while we decided.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step settled its actual box time: smoke $0.05, train $0.81. The estimate is the ceiling you approve; the record keeps what you paid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxjtois3b6rkghgb00yzn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxjtois3b6rkghgb00yzn.png" alt="Train and eval loss over 353 steps on a Tesla T4: eval loss falls from 0.83 to 0.55 and flattens" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Better than the base model, on data it never saw?
&lt;/h2&gt;

&lt;p&gt;The pipeline's &lt;code&gt;improved&lt;/code&gt; gate compares the candidate with the base model on held-out data, and it passed. We then measured both models on the &lt;strong&gt;348-row test split&lt;/strong&gt;, which never reached the trainer: loss fell from &lt;strong&gt;1.955 to 0.218&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A lower loss on your own domain doesn't tell you whether the model got worse at everything else. So we ran a forgetting check with lm-evaluation-harness, with identical settings for both models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;Fine-tuned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ARC-Easy&lt;/td&gt;
&lt;td&gt;0.767&lt;/td&gt;
&lt;td&gt;0.760&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARC-Challenge&lt;/td&gt;
&lt;td&gt;0.527&lt;/td&gt;
&lt;td&gt;0.553&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HellaSwag&lt;/td&gt;
&lt;td&gt;0.647&lt;/td&gt;
&lt;td&gt;0.653&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WinoGrande&lt;/td&gt;
&lt;td&gt;0.740&lt;/td&gt;
&lt;td&gt;0.707&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PIQA&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;td&gt;0.787&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MMLU&lt;/td&gt;
&lt;td&gt;0.768&lt;/td&gt;
&lt;td&gt;0.797&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No change is larger than the noise at these sample sizes (about ±3–4 points). So: &lt;strong&gt;no measurable loss of general ability&lt;/strong&gt;, which is not the same as none. The samples were small, and we say so on the model card.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A person promotes
&lt;/h2&gt;

&lt;p&gt;The run then paused at &lt;code&gt;promote-model&lt;/code&gt;, with the losses on the card. Promotion is a decision, not a side effect. Moving the tag needed its own approval too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it took
&lt;/h2&gt;

&lt;p&gt;Every step was a &lt;code&gt;cairn&lt;/code&gt; CLI command against the engine: install, connect the GPU box, prepare the data, launch, approve, read the findings, promote. The full list, in order, is in the new guide: &lt;strong&gt;&lt;a href="https://docs.cairndev.sh/guides/fine-tune-a-model/" rel="noopener noreferrer"&gt;Fine-tune a model on a GPU box&lt;/a&gt;&lt;/strong&gt;. Swap in your own dataset (Alpaca, prompt/completion or chat) and base model; the pipeline is the same.&lt;/p&gt;

&lt;p&gt;cairn is Apache-2.0 and self-hosted. If you fine-tune models in a team, try the guide on your own data, and tell us where it breaks: &lt;a href="https://github.com/cairndev-sh/cairn" rel="noopener noreferrer"&gt;github.com/cairndev-sh/cairn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cairndev.sh/blog/governed-fine-tune-for-86-cents.html" rel="noopener noreferrer"&gt;cairn blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
