<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ollama on vishctl</title><link>https://vishctl.dev/tags/ollama/</link><description>Recent content in Ollama on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 03 Sep 2026 10:30:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/ollama/index.xml" rel="self" type="application/rss+xml"/><item><title>Running llama.cpp on a 32 GB MacBook Air: A Direct Comparison with Ollama</title><link>https://vishctl.dev/posts/running-llama-cpp-on-32gb-macbook-air/</link><pubDate>Thu, 03 Sep 2026 10:30:00 +0800</pubDate><guid>https://vishctl.dev/posts/running-llama-cpp-on-32gb-macbook-air/</guid><category>ai</category><category>local-llm</category><category>llama-cpp</category><category>ollama</category><category>apple-silicon</category><category>benchmarks</category><description>Compile llama.cpp with Metal support on Apple Silicon, serve local GGUF models, and benchmark prompt eval and generation speeds head-to-head against Ollama.</description><content:encoded><![CDATA[<p>In the <a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/">previous post</a>, I ran Ornith 1.5 9B on my 32 GB MacBook Air using Ollama and recorded baseline token-generation speeds on short prompts. Ollama is great for getting up and running quickly, but under the hood, its inference engine is built on <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a>.</p>
<p>In this post, we go one level down: building and running llama.cpp directly, offloading inference to Apple Silicon&rsquo;s Metal GPU, and comparing performance numbers side by side with Ollama on the exact same model and quantization level.</p>
<h3 id="step-1-build-llamacpp-from-source">Step 1: Build llama.cpp from Source</h3>
<p>Building llama.cpp from source on macOS is fast and straightforward. On Apple Silicon, CMake automatically enables Metal support (<code>GGML_METAL=ON</code>) and compiles GPU compute kernels tailored for Apple&rsquo;s unified memory architecture.</p>
<p>Clone the repository and compile the release binaries using all available CPU cores:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/ggml-org/llama.cpp
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> llama.cpp
</span></span><span class="line"><span class="cl">cmake -B build -DCMAKE_BUILD_TYPE<span class="o">=</span>Release
</span></span><span class="line"><span class="cl">cmake --build build --config Release -j<span class="k">$(</span>sysctl -n hw.logicalcpu<span class="k">)</span>
</span></span></code></pre></div><p>Once the build finishes, the binaries live in <code>build/bin</code>. Add them to your current session&rsquo;s <code>$PATH</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/build/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>To make this permanent across terminal sessions, append it to your shell configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;export PATH=\&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/build/bin:\$PATH\&#34;&#34;</span> &gt;&gt; ~/.zshrc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.zshrc
</span></span></code></pre></div><p>Confirm that the build succeeded:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">llama-server --version
</span></span></code></pre></div><h3 id="step-2-serve-the-model-out-of-the-box-baseline">Step 2: Serve the Model (Out-of-the-Box Baseline)</h3>
<p>llama.cpp can download GGUF models directly from Hugging Face Hub using the <code>-hf</code> flag, caching weights under <code>~/.cache/huggingface/hub</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">llama-server -hf ornith-ai/Ornith-1.5-9B-GGUF --port <span class="m">8080</span> -ngl <span class="m">99</span>
</span></span></code></pre></div><p>Here is what the flags do:</p>
<ul>
<li><code>-hf ornith-ai/Ornith-1.5-9B-GGUF</code>: Resolves the model on Hugging Face and downloads the default <code>Q4_K_M</code> quant.</li>
<li><code>-ngl 99</code>: Offloads all 99 model layers to the GPU (Metal on Apple Silicon unified memory).</li>
<li><code>--port 8080</code>: Binds the OpenAI-compatible HTTP server to port 8080.</li>
</ul>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/llama-cpp-32gb-macbook-air/01-llama-server-start.png"
         alt="Starting llama-server and downloading Ornith 1.5 9B from Hugging Face"/> <figcaption>
            <p>Starting llama-server with automatic Hugging Face download and full Metal GPU offload.</p>
        </figcaption>
</figure>

<p>During startup, llama.cpp downloads both the multimodal vision projector (<code>mmproj-Ornith-1.5-9B-BF16.gguf</code>) and the main model weights (<code>Ornith-1.5-9B-Q4_K_M.gguf</code>), offloads the layers into Metal, initializes inference slots, and listens on port 8080.</p>
<p>We test this default setup with a short greeting prompt through the OpenAI-compatible API:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://localhost:8080/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;model&#34;: &#34;ornith-1.5-9b&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hello!&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">  }&#39;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/llama-cpp-32gb-macbook-air/02-llama-server-first-request.png"
         alt="Calling the llama-server chat completions API"/> <figcaption>
            <p>First chat completion request via curl, returning detailed server-side timing breakdowns.</p>
        </figcaption>
</figure>

<p>The response returns a complete timing breakdown in the <code>timings</code> object:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">prompt eval time = 496.21 ms / 12 tokens (24.18 tokens per second)
</span></span><span class="line"><span class="cl">eval time        = 5527.17 ms / 96 tokens (17.19 tokens per second)
</span></span></code></pre></div><p>Compare that to Ollama&rsquo;s numbers on the same model and quant from the <a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/">earlier post</a>:</p>
<table>
	<thead>
			<tr>
					<th>Metric</th>
					<th>Ollama (Default)</th>
					<th>llama.cpp (Default, no <code>-fa</code>)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Prompt Eval Speed</strong></td>
					<td><strong>37.13 tokens/s</strong></td>
					<td>24.18 tokens/s</td>
			</tr>
			<tr>
					<td><strong>Generation Speed</strong></td>
					<td>16.68 tokens/s</td>
					<td><strong>17.19 tokens/s</strong></td>
			</tr>
	</tbody>
</table>
<p>Generation speed is practically neck-and-neck (~17 tokens/s). However, prompt evaluation (prefill latency) was noticeably slower on vanilla llama.cpp out of the box (24.18 tokens/s vs. Ollama&rsquo;s 37.13 tokens/s).</p>
<p>Ollama enables FlashAttention and sensible batch sizes by default, while raw llama.cpp keeps conservative baseline settings unless configured explicitly.</p>
<h3 id="step-3-verifying-the-quants">Step 3: Verifying the Quants</h3>
<p>Before comparing benchmark numbers, it is critical to confirm that both engines are executing the exact same quantization format. Comparing different quants (e.g. Q4_K_M vs Q8_0) would invalidate any performance conclusions.</p>
<p>Check the quant llama.cpp loaded:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s http://localhost:8080/v1/models <span class="p">|</span> jq
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;ftype&#34;</span><span class="err">:</span> <span class="s2">&#34;Q4_K - Medium&#34;</span>
</span></span></code></pre></div><p>Check the quant Ollama is using:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s http://localhost:11434/api/show -d <span class="s1">&#39;{&#34;model&#34;: &#34;ornith-1.5:9b&#34;}&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;.details.quantization_level&#39;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;Q4_K_M&#34;</span>
</span></span></code></pre></div><p>Both runtimes are confirmed to be executing <code>Q4_K_M</code>. Precision and model size are strictly identical.</p>
<h3 id="step-4-leveling-the-playing-field-with-flash-attention--batch-tuning">Step 4: Leveling the Playing Field with Flash Attention &amp; Batch Tuning</h3>
<p>To give llama.cpp parity with Ollama&rsquo;s runtime optimizations, we restart <code>llama-server</code> with FlashAttention enabled and explicit batch sizes configured:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">llama-server -hf ornith-ai/Ornith-1.5-9B-GGUF --port <span class="m">8080</span> -ngl <span class="m">99</span> -fa on -b <span class="m">2048</span> -ub <span class="m">512</span>
</span></span></code></pre></div><p>Here is what these tuning flags configure:</p>
<ul>
<li><code>-fa on</code> (or <code>--flash-attn on</code>): Enables FlashAttention kernels for Apple Silicon Metal. This significantly accelerates prompt evaluation (prefill) and reduces memory bandwidth overhead.</li>
<li><code>-b 2048</code> (<code>--batch-size</code>): Sets the logical batch size for prompt evaluation.</li>
<li><code>-ub 512</code> (<code>--ubatch-size</code>): Sets the physical micro-batch size dispatched to Metal compute passes, keeping the GPU pipelines fully saturated.</li>
</ul>
<h3 id="step-5-side-by-side-benchmark-on-longer-generation">Step 5: Side-by-Side Benchmark on Longer Generation</h3>
<p>Both engines were given the same prompt:</p>
<blockquote>
<p><code>&quot;give a 25 line story&quot;</code></p>
</blockquote>
<p>A longer generation produces several hundred tokens, giving a much more reliable measurement of steady-state generation speed than short one-liners.</p>
<p>Here are the side-by-side results on the 32 GB MacBook Air:</p>
<table>
	<thead>
			<tr>
					<th>Metric</th>
					<th>Ollama (<code>ornith-1.5:9b</code>)</th>
					<th>llama.cpp (<code>-fa on -b 2048 -ub 512</code>)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Prompt Eval Count</strong></td>
					<td>17 tokens</td>
					<td>18 tokens</td>
			</tr>
			<tr>
					<td><strong>Prompt Eval Speed</strong></td>
					<td>46.01 tokens/s</td>
					<td><strong>48.68 tokens/s</strong></td>
			</tr>
			<tr>
					<td><strong>Generation Count</strong></td>
					<td>496 tokens</td>
					<td>436 tokens</td>
			</tr>
			<tr>
					<td><strong>Generation Speed</strong></td>
					<td>16.42 tokens/s</td>
					<td><strong>17.07 tokens/s</strong></td>
			</tr>
			<tr>
					<td><strong>Total Duration</strong></td>
					<td>30.58 s</td>
					<td><strong>25.85 s</strong></td>
			</tr>
	</tbody>
</table>
<p>With FlashAttention and batch tuning enabled, llama.cpp matches or slightly edges out Ollama across both prompt evaluation (<strong>48.68 vs. 46.01 tokens/s</strong>) and token generation speed (<strong>17.07 vs. 16.42 tokens/s</strong>).</p>
<h4 id="ollama-command--stats">Ollama Command &amp; Stats</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama run ornith-1.5:9b --verbose
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; give a <span class="m">25</span> line story
</span></span><span class="line"><span class="cl">Thinking...
</span></span><span class="line"><span class="cl">...done thinking.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">[</span>story output omitted<span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">total duration:       30.582007541s
</span></span><span class="line"><span class="cl">load duration:        2.821208ms
</span></span><span class="line"><span class="cl">prompt <span class="nb">eval</span> count:    <span class="m">17</span> token<span class="o">(</span>s<span class="o">)</span>
</span></span><span class="line"><span class="cl">prompt <span class="nb">eval</span> duration: 369.481ms
</span></span><span class="line"><span class="cl">prompt <span class="nb">eval</span> rate:     46.01 tokens/s
</span></span><span class="line"><span class="cl"><span class="nb">eval</span> count:           <span class="m">496</span> token<span class="o">(</span>s<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">eval</span> duration:        30.204706s
</span></span><span class="line"><span class="cl"><span class="nb">eval</span> rate:            16.42 tokens/s
</span></span></code></pre></div><h4 id="llamacpp-server-startup--timing-logs">llama.cpp Server Startup &amp; Timing Logs</h4>
<p>Server startup and live slot execution log:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">➜  ~ llama-server -hf ornith-ai/Ornith-1.5-9B-GGUF --port 8080 -ngl 99 -fa on -b 2048 -ub 512
</span></span><span class="line"><span class="cl">0.00.729.018 I cmn  common_param: common_params_print_info: verbosity = 3 (adjust with the `-lv N` CLI arg)
</span></span><span class="line"><span class="cl">0.00.729.664 W srv  llama_server: -----------------
</span></span><span class="line"><span class="cl">0.00.729.667 W srv  llama_server: CORS is set to allow all origins (&#39;*&#39;) and no API key is set
</span></span><span class="line"><span class="cl">0.00.729.668 W srv  llama_server: this can be a security risk (cross-origin attacks)
</span></span><span class="line"><span class="cl">0.00.729.668 W srv  llama_server: more info: https://github.com/ggml-org/llama.cpp/pull/25655
</span></span><span class="line"><span class="cl">0.00.729.669 W srv  llama_server: -----------------
</span></span><span class="line"><span class="cl">0.00.731.435 I srv    load_model: loading model &#39;ornith-ai/Ornith-1.5-9B-GGUF&#39;
</span></span><span class="line"><span class="cl">0.01.177.317 W model has unused tensor blk.32.attn_norm.weight (size = 16384 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.324 W model has unused tensor blk.32.post_attention_norm.weight (size = 16384 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.329 W model has unused tensor blk.32.attn_q.weight (size = 18874368 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.331 W model has unused tensor blk.32.attn_k.weight (size = 2359296 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.333 W model has unused tensor blk.32.attn_v.weight (size = 3440640 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.338 W model has unused tensor blk.32.attn_output.weight (size = 9437184 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.340 W model has unused tensor blk.32.attn_q_norm.weight (size = 1024 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.341 W model has unused tensor blk.32.attn_k_norm.weight (size = 1024 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.343 W model has unused tensor blk.32.ffn_gate.weight (size = 28311552 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.345 W model has unused tensor blk.32.ffn_down.weight (size = 41287680 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.347 W model has unused tensor blk.32.ffn_up.weight (size = 28311552 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.350 W model has unused tensor blk.32.nextn.eh_proj.weight (size = 18874368 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.353 W model has unused tensor blk.32.nextn.enorm.weight (size = 16384 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.354 W model has unused tensor blk.32.nextn.hnorm.weight (size = 16384 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.177.359 W model has unused tensor blk.32.nextn.shared_head_norm.weight (size = 16384 bytes) -- ignoring
</span></span><span class="line"><span class="cl">0.01.799.972 I cmn          init: llama threadpool init, n_threads = 4
</span></span><span class="line"><span class="cl">0.01.959.375 W load_hparams: Qwen-VL models require at minimum 1024 image tokens to function correctly on grounding tasks
</span></span><span class="line"><span class="cl">0.01.959.377 W load_hparams: if you encounter problems with accuracy, try adding --image-min-tokens 1024
</span></span><span class="line"><span class="cl">0.01.959.377 W load_hparams: more info: https://github.com/ggml-org/llama.cpp/issues/16842
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">0.02.172.437 I srv    load_model: loaded multimodal model, &#39;/Users/vishnuhd/.cache/huggingface/hub/models--ornith-ai--Ornith-1.5-9B-GGUF/snapshots/abdd624b12ebf020b767fff532ff44fe552b28c3/mmproj-Ornith-1.5-9B-BF16.gguf&#39;
</span></span><span class="line"><span class="cl">0.02.399.797 I srv    load_model: initializing, n_slots = 4, n_ctx_slot = 262144, kv_unified = &#39;true&#39;
</span></span><span class="line"><span class="cl">0.02.402.615 I srv          init: chat template supports preserving reasoning, consider enabling it via --reasoning-preserve
</span></span><span class="line"><span class="cl">0.02.402.621 I srv  llama_server: model loaded
</span></span><span class="line"><span class="cl">0.02.402.623 I srv  llama_server: listening on http://127.0.0.1:8080
</span></span><span class="line"><span class="cl">0.02.402.623 W srv  llama_server: NOTICE: server default port will be changed to :9931 in a future release
</span></span><span class="line"><span class="cl">0.02.402.623 W srv  llama_server:         ref: https://github.com/ggml-org/llama.cpp/pull/26508
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">0.36.386.802 I slot get_availabl: id  3 | task -1 | selected slot by LRU, t_last = -1
</span></span><span class="line"><span class="cl">0.36.386.826 I slot launch_slot_: id  3 | task 0 | processing task, is_child = 0
</span></span><span class="line"><span class="cl">0.42.586.497 I slot print_timing: id  3 | task 0 | n_gen =    100, tg =  16.98 t/s, tg_3s =  17.15 t/s
</span></span><span class="line"><span class="cl">0.45.633.605 I slot print_timing: id  3 | task 0 | n_gen =    152, tg =  17.01 t/s, tg_3s =  17.07 t/s
</span></span><span class="line"><span class="cl">0.48.679.586 I slot print_timing: id  3 | task 0 | n_gen =    204, tg =  17.03 t/s, tg_3s =  17.07 t/s
</span></span><span class="line"><span class="cl">0.51.718.658 I slot print_timing: id  3 | task 0 | n_gen =    256, tg =  17.04 t/s, tg_3s =  17.11 t/s
</span></span><span class="line"><span class="cl">0.54.746.036 I slot print_timing: id  3 | task 0 | n_gen =    308, tg =  17.07 t/s, tg_3s =  17.18 t/s
</span></span><span class="line"><span class="cl">0.57.778.672 I slot print_timing: id  3 | task 0 | n_gen =    360, tg =  17.08 t/s, tg_3s =  17.15 t/s
</span></span><span class="line"><span class="cl">1.00.817.109 I slot print_timing: id  3 | task 0 | n_gen =    412, tg =  17.08 t/s, tg_3s =  17.11 t/s
</span></span><span class="line"><span class="cl">1.02.233.148 I slot print_timing: id  3 | task 0 | prompt eval time =     369.74 ms /    18 tokens (   20.54 ms per token,    48.68 tokens per second)
</span></span><span class="line"><span class="cl">1.02.233.155 I slot print_timing: id  3 | task 0 |        eval time =   25476.17 ms /   436 tokens (   58.57 ms per token,    17.07 tokens per second)
</span></span><span class="line"><span class="cl">1.02.233.157 I slot print_timing: id  3 | task 0 |       total time =   25845.92 ms /   454 tokens
</span></span><span class="line"><span class="cl">1.02.233.159 I slot print_timing: id  3 | task 0 |    graphs reused =        434
</span></span><span class="line"><span class="cl">1.02.233.187 I slot      release: id  3 | task 0 | stop processing: n_tokens = 453, truncated = 0
</span></span></code></pre></div><p>And the raw curl request (with story output trimmed):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://localhost:8080/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;model&#34;: &#34;ornith-1.5-9b&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Give me a 25 line story&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">  }&#39;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;choices&#34;</span><span class="p">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;finish_reason&#34;</span><span class="p">:</span> <span class="s2">&#34;stop&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;index&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;message&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;assistant&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;[story text omitted]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;reasoning_content&#34;</span><span class="p">:</span> <span class="s2">&#34;[thinking omitted]&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;created&#34;</span><span class="p">:</span> <span class="mi">1788423730</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;model&#34;</span><span class="p">:</span> <span class="s2">&#34;ornith-ai/Ornith-1.5-9B-GGUF&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;system_fingerprint&#34;</span><span class="p">:</span> <span class="s2">&#34;b10712-daef7b687&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;object&#34;</span><span class="p">:</span> <span class="s2">&#34;chat.completion&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;usage&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;completion_tokens&#34;</span><span class="p">:</span> <span class="mi">436</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prompt_tokens&#34;</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;total_tokens&#34;</span><span class="p">:</span> <span class="mi">454</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prompt_tokens_details&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;cached_tokens&#34;</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;chatcmpl-dLoeYkd6rvuFieOvUSr9dBxB798twvZi&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;timings&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;cache_n&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prompt_n&#34;</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prompt_ms&#34;</span><span class="p">:</span> <span class="mf">369.742</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prompt_per_token_ms&#34;</span><span class="p">:</span> <span class="mf">20.54</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prompt_per_second&#34;</span><span class="p">:</span> <span class="mf">48.68</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;predicted_n&#34;</span><span class="p">:</span> <span class="mi">436</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;predicted_ms&#34;</span><span class="p">:</span> <span class="mf">25476.174</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;predicted_per_token_ms&#34;</span><span class="p">:</span> <span class="mf">58.57</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;predicted_per_second&#34;</span><span class="p">:</span> <span class="mf">17.07</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="key-takeaways">Key Takeaways</h3>
<ol>
<li><strong>Ollama is not faster than llama.cpp</strong>: Under the hood, Ollama is llama.cpp. Its initial out-of-the-box advantage in prompt evaluation comes entirely from default runtime tuning (FlashAttention and batch sizes), not a secret runtime or different quantization.</li>
<li><strong>FlashAttention is essential on Apple Silicon</strong>: Enabling <code>-fa on</code> doubled prompt evaluation speed from <strong>24.18 tokens/s to 48.68 tokens/s</strong>, drastically cutting time-to-first-token.</li>
<li><strong>Consistent throughput</strong>: At 9B parameters with Q4_K_M quantization, Apple Silicon unified memory sustains a rock-solid <strong>~17.1 tokens/sec</strong>.</li>
<li><strong>Why run llama.cpp directly?</strong>: Ollama provides exceptional developer ergonomics for local apps and testing. But direct llama.cpp gives you full control over context allocation, slot limits, KV cache quantization (<code>-ctk</code>, <code>-ctv</code>), and immediate access to upstream features and bugfixes.</li>
</ol>
<p>In the next post, we will explore KV cache quantization and memory profiling to see how far we can stretch long-context windows on a 32 GB machine.</p>
]]></content:encoded></item><item><title>Running Ollama on a 32 GB MacBook Air: A Practical First Setup</title><link>https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/</link><pubDate>Thu, 03 Sep 2026 09:30:00 +0800</pubDate><guid>https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/</guid><category>ai</category><category>local-llm</category><category>ollama</category><category>apple-silicon</category><category>homelab</category><description>Install Ollama, pull and run local models, and call the local API on a 32 GB Apple-silicon MacBook Air.</description><content:encoded><![CDATA[<p>I have a 32 GB MacBook Air. It is not a workstation GPU box, but its unified memory makes it a surprisingly capable machine for local models, provided I choose models that fit and keep expectations sensible.</p>
<p>This is the first post in a small, practical series about running models locally. I am starting with Ollama because it gets a model running quickly without building a runtime from source or hand-managing dependencies.</p>
<h3 id="why-ollama">Why Ollama</h3>
<p><a href="https://ollama.com/">Ollama</a> manages model downloads, exposes a straightforward CLI, and starts a local HTTP API. On Apple silicon, it supports the Apple GPU; Ollama&rsquo;s current macOS requirement is Sonoma (14) or newer. The app stores models and configuration under <code>~/.ollama</code>. <a href="https://docs.ollama.com/macos">The macOS documentation</a> is the useful reference for install, storage, and logs.</p>
<p>Under the hood, Ollama packages model management and an API around inference backends including <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a>. That distinction is useful: Ollama is the convenient front door; llama.cpp is a lower-level route I can use later when I want to compare runtimes directly.</p>
<h3 id="step-1-install-ollama">Step 1: Install Ollama</h3>
<p>Download the macOS app from <a href="https://ollama.com/download">ollama.com/download</a>, mount the DMG, and drag Ollama to <code>/Applications</code>. Start it once. If the CLI is not already available, the app will offer to add it to your path.</p>
<p>Confirm that both the CLI and the local server are available:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama --version
</span></span><span class="line"><span class="cl">ollama list
</span></span><span class="line"><span class="cl">ollama ps
</span></span></code></pre></div><p>On a new installation, <code>ollama list</code> and <code>ollama ps</code> should be empty. The first reports downloaded models; the second reports models currently loaded into memory.</p>
<h3 id="step-2-pull-a-model">Step 2: Pull a Model</h3>
<p>For this machine, I started with Ornith 1.5 9B:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama pull ornith-1.5:9b
</span></span></code></pre></div><p>This feels deliberately familiar if you work with containers: <code>ollama pull</code> downloads the model and its layers, while <code>ollama run</code> starts an interactive session. The current Ollama build of <code>ornith-1.5:9b</code> is 6.6 GB with a 256K context window, which is a comfortable starting point on a 32 GB laptop. <a href="https://ollama.com/library/ornith-1.5">Ollama&rsquo;s model page</a> lists the available tags; the 35B download is 23 GB, so I would not make that the default on an Air.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/ollama-32gb-macbook-air/01-pull-ornith-1-5-9b.png"
         alt="Pulling Ornith 1.5 9B, then confirming the local model"/> <figcaption>
            <p>Pulling Ornith 1.5 9B, then confirming it is available locally.</p>
        </figcaption>
</figure>

<p>After the pull completes, confirm it is available:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama list
</span></span></code></pre></div><h3 id="step-3-run-it-interactively">Step 3: Run It Interactively</h3>
<p>Start a chat session with the model:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama run ornith-1.5:9b
</span></span></code></pre></div><p>Use a question that resembles the work you actually do. I tested a simple greeting first, then moved on to infrastructure questions. Exit the interactive prompt with <code>/exit</code> or <code>Ctrl-D</code>.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/ollama-32gb-macbook-air/02-run-ornith-1-5-9b.png"
         alt="An interactive Ornith 1.5 9B session in the terminal"/> <figcaption>
            <p>A first interactive conversation with the locally running model.</p>
        </figcaption>
</figure>

<p>For an initial sanity check, the model was responsive and produced a natural answer. That is useful confirmation that the model loads and runs locally, but it is not a benchmark. A real comparison needs the same prompt, context length, generation settings, and output length.</p>
<h3 id="step-4-inspect-performance-with---verbose">Step 4: Inspect Performance with <code>--verbose</code></h3>
<p>Ollama&rsquo;s <code>--verbose</code> flag is a quick way to see timings after every response:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama run ornith-1.5:9b --verbose
</span></span></code></pre></div><p>On this MacBook Air, my short greeting test produced 114 output tokens at <strong>16.68 tokens/sec</strong>, with a total duration of <strong>7.22 seconds</strong>. The model&rsquo;s thinking trace was visible before its answer.</p>
<p>I ran the same kind of test with Gemma 4 E4B:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama run gemma4:e4b --verbose
</span></span></code></pre></div><p>That run produced 228 output tokens at <strong>27.62 tokens/sec</strong>, with a total duration of <strong>8.49 seconds</strong>. It is faster in this small test, but it also generated a different and longer response. These figures are useful as a personal baseline, not as an apples-to-apples model ranking.</p>
<h3 id="ornith-15-9b-vs-gemma-4-e4b">Ornith 1.5 9B vs. Gemma 4 E4B</h3>
<p>Both models fit well on a 32 GB MacBook Air, but they are aimed at slightly different trade-offs.</p>
<table>
	<thead>
			<tr>
					<th>Model</th>
					<th>What I observed</th>
					<th>Practical fit</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>ornith-1.5:9b</code></td>
					<td>6.6 GB download; 16.68 tokens/sec in my short verbose run</td>
					<td>A capable 9B-class, text-and-image model with plenty of memory headroom</td>
			</tr>
			<tr>
					<td><code>gemma4:e4b</code></td>
					<td>9.6 GB download; 27.62 tokens/sec in my different short verbose run</td>
					<td>An efficient edge model for local chat, reasoning, coding, and multimodal work</td>
			</tr>
	</tbody>
</table>
<p>The <code>E</code> in Gemma 4 E4B means <strong>effective</strong> parameters. Ollama describes E4B as a 4.5B-effective-parameter edge model (8B including embeddings), with a 128K context window and text, image, and audio support. It is designed to do useful local work without the memory cost of the larger Gemma 4 workstation models. <a href="https://ollama.com/library/gemma4">The Gemma 4 library page</a> has the current tags, sizes, and capabilities.</p>
<p>In practice, I would start with <code>ornith-1.5:9b</code> if I want the smaller download and a roomy 256K context window, or <code>gemma4:e4b</code> if I want the efficient Gemma 4 feature set. Neither of these two quick runs says which model is universally better; use the prompts you care about and record the result.</p>
<h3 id="step-5-call-the-local-api">Step 5: Call the Local API</h3>
<p>The terminal chat is only the first test. Ollama exposes an API locally at <code>http://localhost:11434/api</code>, so the model can be part of a script or an application.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://localhost:11434/api/chat <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;model&#34;: &#34;ornith-1.5:9b&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hello!&#34;}],
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;stream&#34;: false
</span></span></span><span class="line"><span class="cl"><span class="s1">  }&#39;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/ollama-32gb-macbook-air/03-ornith-local-api.png"
         alt="Calling the local Ollama chat API with curl"/> <figcaption>
            <p>Calling the same model through Ollama&rsquo;s local chat API.</p>
        </figcaption>
</figure>

<p>The response includes the answer and useful timing fields. Keep this endpoint local by default. If I later expose it to another device, I will put authentication and a proper reverse proxy in front of it. I will not publish port 11434 directly. <a href="https://docs.ollama.com/api/introduction">Ollama&rsquo;s API documentation</a> covers the local base URL and client libraries.</p>
<h3 id="step-6-try-the-app-ui">Step 6: Try the App UI</h3>
<p>The CLI is great for testing and scripts, but the Ollama app also gives me a simple chat interface. Here, Ornith is selected in the model picker and used for a weather question.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/ollama-32gb-macbook-air/04-ollama-app-ornith.png"
         alt="The Ollama app with Ornith 1.5 9B selected"/> <figcaption>
            <p>The Ollama app with Ornith 1.5 9B selected for a web-assisted prompt.</p>
        </figcaption>
</figure>

<p>The app can also give a model access to web tools. In this example, Ornith searched for current Singapore weather before answering. That extends the model with fresh online information when the tool is enabled, but it does not replace the model&rsquo;s built-in training knowledge. Treat retrieved results as sources to verify, especially for technical or time-sensitive answers.</p>
<p>The UI is useful when I want to compare prompts casually. The local API is the path I will use when I want to integrate models into tooling.</p>
<h3 id="commands-worth-remembering">Commands Worth Remembering</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Download without entering an interactive chat</span>
</span></span><span class="line"><span class="cl">ollama pull ornith-1.5:9b
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start an interactive chat</span>
</span></span><span class="line"><span class="cl">ollama run ornith-1.5:9b
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show downloaded models and their disk usage</span>
</span></span><span class="line"><span class="cl">ollama list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show models currently loaded by the runner</span>
</span></span><span class="line"><span class="cl">ollama ps
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a model I no longer need</span>
</span></span><span class="line"><span class="cl">ollama rm ornith-1.5:9b
</span></span></code></pre></div><p>Models are not small. Treat <code>ollama pull</code> the same way you would a sizeable <code>docker pull</code>: check disk space before collecting a pile of models &ldquo;just in case.&rdquo;</p>
<h3 id="what-this-macbook-air-is-good-at">What This MacBook Air Is Good At</h3>
<p>An 8B to 9B class model is a good fit for private note summarisation, explaining logs, drafting YAML, lightweight coding help, and experimenting with local integrations. This is not where I expect a 70B-class model to be effortless or where I would host production inference.</p>
<p>There are many ways to run models locally, but Ollama is an excellent Apple-silicon starting point. It gets the plumbing out of the way so I can spend time evaluating the models themselves.</p>
<p>Next, I will run llama.cpp directly on the same machine, then see whether a vLLM setup is worth comparing. Stay tuned.</p>
]]></content:encoded></item></channel></rss>