<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>smart on Antonio Space</title>
    <link>/tags/smart/</link>
    <description>Recent content in smart on Antonio Space</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Thu, 15 Oct 2026 20:00:00 +0800</lastBuildDate><atom:link href="/tags/smart/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Predicting Disk Failures with a Small LLM, Part 1: A Working Pipeline and a Useless Model</title>
      <link>/posts/backblaze-disk-failure-part-1/</link>
      <pubDate>Thu, 15 Oct 2026 20:00:00 +0800</pubDate>
      
      <guid>/posts/backblaze-disk-failure-part-1/</guid>
      <description>Most hardware I have operated tells me only part of what is happening inside it. A disk reports a handful of counters. A switch reports interface errors. A GPU reports temperature and a few error codes. The interesting physics, firmware decisions and wear sit underneath, and I mostly see their side effects.
I wanted to know how far those side effects can take you. Can a model look at a week of a drive&amp;rsquo;s self-reported telemetry and say whether it will fail soon?</description>
      <content>&lt;p&gt;Most hardware I have operated tells me only part of what is happening inside it. A disk reports a handful of counters. A switch reports interface errors. A GPU reports temperature and a few error codes. The interesting physics, firmware decisions and wear sit underneath, and I mostly see their side effects.&lt;/p&gt;
&lt;p&gt;I wanted to know how far those side effects can take you. Can a model look at a week of a drive&amp;rsquo;s self-reported telemetry and say whether it will fail soon? And can a small language model, fine-tuned on my own GPU, learn to do that?&lt;/p&gt;
&lt;p&gt;This post covers the first attempt. The pipeline works end to end. The model it produced does not predict anything useful. Both results are worth writing down.&lt;/p&gt;
&lt;h2 id=&#34;why-disk-failure-is-hard-to-predict&#34;&gt;Why disk failure is hard to predict&lt;/h2&gt;
&lt;p&gt;A drive&amp;rsquo;s failure depends on things the telemetry only partly captures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Workload.&lt;/strong&gt; The same drive model behaves differently under a backup archive than under a busy database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware design and firmware.&lt;/strong&gt; Vendors decide which events to count, when to remap a sector, and what each SMART attribute means. The same attribute number can carry different semantics across vendors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Age.&lt;/strong&gt; Wear accumulates, but early-life failures also exist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temperature and operating conditions.&lt;/strong&gt; Heat, vibration, power events and the chassis a drive sits in all matter, and only some of it is recorded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also matters what kind of device we are talking about.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mechanical hard drives&lt;/strong&gt; have spinning platters, moving heads and motors. Many failures develop gradually: the surface degrades, sectors become hard to read, and the drive remaps them. Counters such as reallocated and pending sectors can rise before the drive dies. Some failures, such as a head crash or a motor or electronics fault, are sudden and may leave little trace in SMART beforehand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Semiconductor devices&lt;/strong&gt; such as SSDs have no moving parts. NAND flash wears with program/erase cycles, which drives expose through wear and spare-block indicators. But controller or firmware faults can take a device down abruptly, and the attributes that matter are different and often vendor-specific. A model trained on hard drive counters says nothing about SSDs.&lt;/p&gt;
&lt;p&gt;This series stays with mechanical drives, because that is where the public data is deepest.&lt;/p&gt;
&lt;h2 id=&#34;the-data-backblaze-drive-stats&#34;&gt;The data: Backblaze Drive Stats&lt;/h2&gt;
&lt;p&gt;Backblaze publishes daily snapshots of every drive in its data centers: model, serial number, capacity, a failure flag, and raw and normalized SMART values. I used Q1 2025, which is 90 daily CSV files in one archive (&lt;code&gt;data_Q1_2025.zip&lt;/code&gt;, about 1 GB).&lt;/p&gt;
&lt;p&gt;Mixing drive models would mix vendors&amp;rsquo; SMART semantics, so I picked one model: the &lt;strong&gt;Toshiba MG07ACA14TA&lt;/strong&gt;, a 14 TB drive with a large population in the fleet. Filtering the quarter to that model gave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3,385,931 daily observations&lt;/li&gt;
&lt;li&gt;133 unique drives that failed during the quarter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From each daily row I kept seven raw SMART fields:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SMART ID&lt;/th&gt;
&lt;th&gt;Common meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Reallocated sectors count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Power-on hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;194&lt;/td&gt;
&lt;td&gt;Temperature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;td&gt;Reallocation event count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;197&lt;/td&gt;
&lt;td&gt;Current pending sector count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;Offline uncorrectable sectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;td&gt;UDMA CRC error count&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Most of these describe the media surface, plus age, heat and cabling errors. That is a narrow window into the drive, and it is deliberate. I wanted to start with the counters people usually watch.&lt;/p&gt;
&lt;h2 id=&#34;framing-the-prediction-task&#34;&gt;Framing the prediction task&lt;/h2&gt;
&lt;p&gt;The question I gave the model:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Given seven consecutive daily observations of one drive, will it fail during the following seven days?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer is a single JSON object:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;failure_within_7_days&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;How the examples are built matters more than the model, so I was strict about it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One example per drive.&lt;/strong&gt; A drive that contributes many overlapping windows would dominate training and could leak between splits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Positive examples end one to seven days before the recorded failure.&lt;/strong&gt; The history never includes the failure day itself, so the model cannot see the event it is supposed to predict.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Negative examples need a complete seven-day follow-up without failure.&lt;/strong&gt; A drive that disappears from the data three days later is not a confirmed healthy example.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Histories with missing selected SMART values are rejected.&lt;/strong&gt; I would rather have fewer examples than teach the model to read gaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serial numbers and failure flags are excluded from the input.&lt;/strong&gt; Either would let the model cheat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Splits are by drive, stratified, and random.&lt;/strong&gt; No drive appears in more than one split. The split is &lt;em&gt;not&lt;/em&gt; chronological, which I will come back to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Healthy drives are sampled.&lt;/strong&gt; Using every healthy drive would make failures vanishingly rare. The price is that my test set has far more failures than the real fleet, so its accuracy numbers do not transfer to production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The examples are stored as JSONL in Alpaca format, with &lt;code&gt;instruction&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;output&lt;/code&gt; fields.&lt;/p&gt;
&lt;!-- TODO: paste one real (shortened) record from data/train.jsonl here so readers see the exact instruction and input format. --&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Failures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;train.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;779&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;val.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;167&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That is 114 usable failed drives out of 133. The rest were lost to the missing-value and window rules.&lt;/p&gt;
&lt;h2 id=&#34;the-local-setup&#34;&gt;The local setup&lt;/h2&gt;
&lt;p&gt;Everything ran on my Linux box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NVIDIA RTX 3060 with 12 GB VRAM (11.63 GiB usable), 16 GB RAM&lt;/li&gt;
&lt;li&gt;Python 3.12.3, PyTorch 2.14.1+cu130&lt;/li&gt;
&lt;li&gt;Soup 0.75.2 (the &lt;code&gt;soup-cli&lt;/code&gt; package), which wraps Transformers, PEFT and TRL behind one YAML config&lt;/li&gt;
&lt;li&gt;Workspace and Hugging Face cache on &lt;code&gt;/srv/storage&lt;/code&gt;, so model downloads don&amp;rsquo;t fill the system disk:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;export HF_HOME&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;/srv/storage/backblaze-soup/models/huggingface
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;soup doctor
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;soup doctor&lt;/code&gt; checks the Python, CUDA and library versions before you spend time on a run. It is worth running after every environment change. I learned that later, the hard way.&lt;/p&gt;
&lt;h2 id=&#34;fine-tuning-qwen25-15b-with-qlora&#34;&gt;Fine-tuning Qwen2.5-1.5B with QLoRA&lt;/h2&gt;
&lt;p&gt;I chose &lt;strong&gt;Qwen2.5-1.5B-Instruct&lt;/strong&gt;: small enough to train comfortably on a 3060, and already good at following a &amp;ldquo;reply with JSON&amp;rdquo; instruction.&lt;/p&gt;
&lt;p&gt;Full fine-tuning of 1.5 billion parameters does not fit in 12 GB. &lt;strong&gt;QLoRA&lt;/strong&gt; makes it fit. The base weights are loaded in 4-bit and frozen, and only small low-rank adapter matrices are trained. The adapter is a few megabytes, and the original model stays untouched.&lt;/p&gt;
&lt;p&gt;This is the whole configuration:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;base&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Qwen/Qwen2.5-1.5B-Instruct&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;task&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;sft&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;backend&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;transformers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;data&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;train&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/srv/storage/backblaze-soup/data/train.jsonl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;format&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;alpaca&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;val_split&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;max_length&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;2048&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;train_on_responses_only&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;training&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;epochs&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;lr&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.0001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;batch_size&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;gradient_accumulation_steps&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;gradient_checkpointing&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;quantization&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;4bit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;seed&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;42&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;data_seed&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;42&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;logging_steps&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;save_steps&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;lora&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;r&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;alpha&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;32&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;dropout&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0.05&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;target_modules&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;auto&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;output&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/srv/storage/backblaze-soup/output/qwen-backblaze-v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few choices worth explaining:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Batch size 1 with gradient accumulation 8&lt;/strong&gt; gives an effective batch of 8 without holding eight long sequences in memory at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gradient checkpointing&lt;/strong&gt; recomputes activations during the backward pass instead of storing them. It is slower, but it saves memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;train_on_responses_only&lt;/code&gt;&lt;/strong&gt; computes loss only on the answer, not on the long SMART history in the prompt. I want the model graded on its prediction, not on copying numbers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;val_split: 0.1&lt;/code&gt;&lt;/strong&gt; means Soup held back 10% of &lt;code&gt;train.jsonl&lt;/code&gt; for its own validation. My separate &lt;code&gt;val.jsonl&lt;/code&gt; was not used during training.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;soup train --config soup.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The run took 88 optimizer steps and about six minutes, with GPU memory peaking around 3.5 GB. I did not enable layer streaming. The reported loss fell from 1.0847 to 0.0198.&lt;/p&gt;
&lt;p&gt;To see what the GPU was doing during that run, I later reran exactly the same configuration into a separate output folder and watched it in Grafana, with &lt;code&gt;nvidia-smi&lt;/code&gt; sampling once per second alongside. The rerun matched closely: 88 steps, 408 seconds of training (about eight minutes including model and data loading), and loss from 1.0812 to 0.0198. The first logged loss differs slightly from the original 1.0847, a reminder that a fixed seed does not guarantee bit-identical GPU runs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/images/backblaze-v1-gpu-grafana.png&#34; alt=&#34;Grafana dashboard of the RTX 3060 during the QLoRA training rerun: temperature, utilization, power, memory, clocks and fan speed&#34;&gt;&lt;/p&gt;
&lt;p&gt;Two things stand out. Soup reported a 3.5 GB peak, but the GPU showed up to 8.1 GiB in use. My reading is that Soup reports memory PyTorch allocated for tensors, while the driver also counts the CUDA context and memory PyTorch&amp;rsquo;s caching allocator has reserved but not handed out. Both numbers are real; they answer different questions. The card was also busy without being maxed out: once training started, utilization averaged about 92%, power peaked at 133 W against a 170 W limit, and temperature rose from 59 °C to 71 °C. For a 1.5B model in 4-bit with batch size 1, the 3060 has headroom.&lt;/p&gt;
&lt;p&gt;On paper, that looks like success.&lt;/p&gt;
&lt;h2 id=&#34;evaluating-it&#34;&gt;Evaluating it&lt;/h2&gt;
&lt;p&gt;I ran the 168 test examples through the fine-tuned adapter and through the original model, then compared the answers with the labels.&lt;/p&gt;
&lt;p&gt;The fine-tuned model returned valid, bare JSON for all 168 cases. It also answered &lt;code&gt;false&lt;/code&gt; for all 168:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Predicted failure&lt;/th&gt;
&lt;th&gt;Predicted healthy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Actually failed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Actually healthy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Accuracy was 89.3%. Failure recall was 0%. A model that always says &amp;ldquo;no failure&amp;rdquo; gets exactly the same score, because 150 of the 168 test drives are healthy. The fine-tuned model learned the answer format and the majority class, and missed every drive that actually failed.&lt;/p&gt;
&lt;h2 id=&#34;a-bug-in-my-own-evaluation&#34;&gt;A bug in my own evaluation&lt;/h2&gt;
&lt;p&gt;The original model at first looked even worse: my script marked all of its answers invalid. When I inspected the first five, each was perfectly reasonable JSON, wrapped in a Markdown code block:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;```json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;failure_within_7_days&amp;#34;: false
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My parser expected bare JSON and rejected the fences. So &amp;ldquo;0% valid&amp;rdquo; described my evaluation code, not the model. Those five answers all predicted &lt;code&gt;false&lt;/code&gt; too, but five answers are not a baseline, and the full corrected comparison is still pending.&lt;/p&gt;
&lt;h2 id=&#34;what-i-take-from-this&#34;&gt;What I take from this&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local fine-tuning is accessible.&lt;/strong&gt; A consumer GPU, a 1.5B model and QLoRA got me from raw CSVs to a trained adapter on one machine, and the training run itself took six minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A low training loss is not a useful predictor.&lt;/strong&gt; With 90% healthy examples and a tiny fixed answer, the cheapest way to reduce loss is to always say no.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid JSON is not a useful predictor either.&lt;/strong&gt; Format compliance is easy to measure and easy to mistake for progress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accuracy is the wrong headline metric&lt;/strong&gt; for rare events. I&amp;rsquo;ll report failure recall, precision and false alerts from now on.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;next&#34;&gt;Next&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve built a balanced training set with 79 failed and 79 healthy drives, so &amp;ldquo;always no&amp;rdquo; stops being a free win. The plan is to train a fresh adapter from the original base model for three epochs on it, and to compare it on the validation set rather than repeatedly on the test set. I also want a tabular baseline, such as XGBoost or LightGBM on the same features, to see whether a language model adds anything at all. Last, I want an evaluation on a later time period before drawing any operational conclusions.&lt;/p&gt;
&lt;p&gt;That second training run has not produced a result yet.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
