<?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>Gpu on vishctl</title><link>https://vishctl.dev/tags/gpu/</link><description>Recent content in Gpu on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 08 Sep 2026 13:30:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/gpu/index.xml" rel="self" type="application/rss+xml"/><item><title>Run vLLM on Kubernetes with Minikube, WSL2 and NVIDIA GPU</title><link>https://vishctl.dev/posts/vllm-on-wsl2-minikube/</link><pubDate>Tue, 08 Sep 2026 13:30:00 +0800</pubDate><guid>https://vishctl.dev/posts/vllm-on-wsl2-minikube/</guid><category>ai</category><category>local-llm</category><category>vllm</category><category>kubernetes</category><category>minikube</category><category>wsl2</category><category>gpu</category><category>nvidia</category><description>A practical guide to running vLLM on Kubernetes in WSL2 with Minikube, configuring NVIDIA GPU passthrough, and serving an OpenAI-compatible API.</description><content:encoded><![CDATA[<p>This is the <a href="https://docs.vllm.ai/">vLLM</a> entry in my local AI series. After testing <a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/">Ollama</a>, <a href="https://vishctl.dev/posts/running-llama-cpp-on-32gb-macbook-air/">llama.cpp</a>, and <a href="https://vishctl.dev/posts/running-freetoken-on-8gb-laptop-gpu/">FreeToken</a>, I wanted to run vLLM as a Kubernetes Deployment on my Windows/WSL2 setup.</p>
<p>The plan was simple: deploy vLLM, request <code>nvidia.com/gpu: 1</code>, expose an OpenAI-compatible API endpoint through a Service, and tie it into the Kubernetes workflows I write about regularly.</p>
<p>Getting a GPU into Kubernetes on WSL2 turned into an investigation. Not because vLLM is hard, but because container runtimes and nested clusters handle GPU passthrough in non-obvious ways. Here is what happened, how the device plugin and node prerequisites work, and how to get a working GPU cluster running with Minikube.</p>
<hr>
<h2 id="what-youll-build">What You&rsquo;ll Build</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Windows
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">WSL2 Ubuntu
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Docker Engine + NVIDIA Container Toolkit
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Minikube
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Kubernetes
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ├── NVIDIA GPU Operator
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ├── NVIDIA Device Plugin
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   └── vLLM
</span></span><span class="line"><span class="cl">         │
</span></span><span class="line"><span class="cl">         ▼
</span></span><span class="line"><span class="cl">      NVIDIA GPU
</span></span></code></pre></div><p>By the end of this guide, you&rsquo;ll have vLLM running on Kubernetes with GPU acceleration and serving an OpenAI-compatible API.</p>
<hr>
<h2 id="the-test-rig">The Test Rig</h2>
<p>My test machine for this run:</p>
<ul>
<li><strong>Host Machine:</strong> Windows 11 Laptop</li>
<li><strong>GPU:</strong> NVIDIA GeForce RTX 4070 Laptop GPU (8.0 GiB VRAM)</li>
<li><strong>Host Memory:</strong> 32 GB DDR5</li>
<li><strong>WSL2 Environment:</strong> Ubuntu 24.04 LTS (noble) with systemd enabled</li>
<li><strong>NVIDIA Drivers:</strong> Driver version 616.56 / CUDA 13.4 user-mode driver</li>
<li><strong>Kubernetes:</strong> Minikube v1.39.0 provisioning Kubernetes v1.37.0</li>
<li><strong>Model:</strong> Qwen3.5-0.8B (<code>Qwen/Qwen3.5-0.8B</code>) served by vLLM v0.28.0</li>
</ul>
<hr>
<h2 id="troubleshooting-vllm-gpu-support-on-docker-desktop-wsl2-kubernetes">Troubleshooting vLLM GPU Support on Docker Desktop WSL2 Kubernetes</h2>
<p>Docker Desktop offers a one-click Kubernetes cluster in its settings. Turning it on provisions a single-node cluster named <code>desktop-control-plane</code> running on <code>containerd://2.3.4</code>.</p>
<p>On the WSL2 host, <code>nvidia-smi</code> works fine, and running standalone GPU containers (<code>docker run --gpus all</code>) works without issues. The next step was applying the standard NVIDIA Kubernetes device plugin DaemonSet to enable GPU scheduling:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.16.2/deployments/static/nvidia-device-plugin.yml
</span></span></code></pre></div><p>The DaemonSet pod started, but immediately stalled in a retry loop:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">E... Incompatible strategy detected auto
</span></span><span class="line"><span class="cl">E... If this is a GPU node, did you configure the NVIDIA Container Toolkit?
</span></span><span class="line"><span class="cl">I... No devices found. Waiting indefinitely.
</span></span></code></pre></div><p>Checking the node capacity confirmed the issue:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl describe node desktop-control-plane <span class="p">|</span> grep -A5 <span class="s2">&#34;Capacity:\|Allocatable:&#34;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/01-docker-desktop-k8s-no-gpu.png"
         alt="Docker Desktop Kubernetes node missing GPU resources"/> <figcaption>
            <p>Checking Docker Desktop&rsquo;s desktop-control-plane: nvidia-smi finds the RTX 4070, but the node exposes zero GPU capacity.</p>
        </figcaption>
</figure>

<p>In this tested Docker Desktop Kubernetes setup, GPU resources were not exposed to the Kubernetes node. Despite GPU access working in WSL2 and standalone Docker containers, the Kubernetes node could not expose <code>nvidia.com/gpu</code> to the device plugin.</p>
<hr>
<h2 id="how-kubernetes-detects-nvidia-gpus-using-the-nvidia-device-plugin">How Kubernetes Detects NVIDIA GPUs Using the NVIDIA Device Plugin</h2>
<p>The <a href="https://github.com/NVIDIA/k8s-device-plugin">NVIDIA Kubernetes Device Plugin</a> is a DaemonSet that exposes GPU hardware to the Kubernetes control plane. It does not run inference, and it does not install drivers or container runtimes.</p>
<p>Under standard <a href="https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/">Kubernetes GPU scheduling</a>, pods request GPU resources by specifying limits for extended resources. The device plugin integrates with Kubelet through the Kubernetes Device Plugin API over gRPC, using a Unix domain socket at <code>/var/lib/kubelet/device-plugins/kubelet.sock</code>. Its responsibility comes down to three functions:</p>
<ol>
<li><strong>Discovery:</strong> The plugin queries the host system using NVML (NVIDIA Management Library) to check how many physical GPUs are present and verify their health status.</li>
<li><strong>Registration:</strong> It registers with Kubelet and advertises the discovered GPUs as an extended allocatable resource named <code>nvidia.com/gpu</code>.</li>
<li><strong>Allocation:</strong> When a pod requesting <code>nvidia.com/gpu: 1</code> gets scheduled to the node, Kubelet calls the plugin&rsquo;s <code>Allocate</code> gRPC endpoint. The plugin picks a healthy GPU and sends back the device IDs and environment variables (<code>NVIDIA_VISIBLE_DEVICES=&lt;UUID&gt;</code>) to Kubelet. Kubelet then passes those variables to the container runtime so the container gets access to <code>/dev/nvidia*</code>.</li>
</ol>
<h3 id="worker-node-prerequisites">Worker Node Prerequisites</h3>
<p>Because the device plugin only handles discovery and Kubelet registration, it assumes the worker node already has a functional GPU stack. For worker nodes in any Kubernetes cluster, three layers must be in place before the device plugin can run:</p>
<ol>
<li><strong>Host Kernel Drivers:</strong> The node OS must have the NVIDIA kernel modules loaded (<code>nvidia.ko</code>, <code>nvidia-uvm.ko</code>) and device nodes created in <code>/dev</code>. Running <code>nvidia-smi</code> on the host must return clean output.</li>
<li><strong>NVIDIA Container Toolkit:</strong> Packages such as <code>libnvidia-container</code> and <code>nvidia-container-toolkit</code> must be installed on the host. Refer to the <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html">NVIDIA Container Toolkit install guide</a> for distribution setup. This toolkit includes the OCI prestart hook that inspects container environment variables and mounts host GPU driver libraries into the target container.</li>
<li><strong>Runtime Configuration:</strong> The node container runtime (containerd or Docker) must have the NVIDIA runtime configured in its daemon settings (such as <code>/etc/containerd/config.toml</code> or <code>/etc/docker/daemon.json</code>) and set up to handle CDI (Container Device Interface) or NVIDIA runtime hooks.</li>
</ol>
<p>If any of those three pieces is missing or unconfigured, the device plugin pod will fail during startup or report zero allocatable GPUs.</p>
<hr>
<h2 id="why-docker-desktop-kubernetes-cannot-detect-my-nvidia-gpu">Why Docker Desktop Kubernetes Cannot Detect My NVIDIA GPU</h2>
<p>Based on the runtime architecture observed in this setup, the issue comes down to how Docker Desktop isolates its cluster node.</p>
<p><code>desktop-control-plane</code> runs inside Docker Desktop&rsquo;s private utility VM (<code>docker-desktop</code>), isolated using <strong><code>sysbox-runc</code></strong> rather than the standard OCI runtime <code>runc</code>. Sysbox provides nested container virtualization to spin up systemd, kubelet, and containerd within an unprivileged container environment.</p>
<p>In this setup, that isolation layer prevents GPU passthrough:</p>
<ul>
<li><strong>Missing OCI Passthrough:</strong> The node container was not launched with NVIDIA GPU passthrough flags (<code>--gpus all</code>). Inside the sandbox, containerd cannot access NVIDIA device nodes (<code>/dev/nvidia*</code>) or the WSL2 DirectX driver mapping (<code>/usr/lib/wsl/lib</code>).</li>
<li><strong>Locked-Down Lifecycle:</strong> You cannot configure the NVIDIA Container Toolkit inside the node&rsquo;s containerd because Docker Desktop internally supervises the container.</li>
<li><strong>Daemon Defaults Do Not Propagate:</strong> Setting <code>nvidia</code> as the default OCI runtime in <code>docker-desktop</code> WSL does not alter the <code>sysbox-runc</code> runtime used by the Kubernetes node container.</li>
</ul>
<p>Docker has an open roadmap issue requesting native GPU passthrough for Docker Desktop Kubernetes. It remains an architectural boundary in the current setup.</p>
<p>Because of these issues, I pivoted to running Minikube with WSL2 using native Docker.</p>
<hr>
<h2 id="installing-docker-ce-and-containerd-inside-wsl2">Installing Docker CE and Containerd Inside WSL2</h2>
<p>To satisfy those node prerequisites and bypass Docker Desktop&rsquo;s VM isolation, we install the native Docker Community Edition engine directly inside the Ubuntu 24.04 WSL2 environment:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Add Docker&#39;s official GPG key</span>
</span></span><span class="line"><span class="cl">sudo mkdir -p /etc/apt/keyrings
</span></span><span class="line"><span class="cl">sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
</span></span><span class="line"><span class="cl">sudo chmod a+r /etc/apt/keyrings/docker.asc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set up the repository</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;deb [arch=</span><span class="k">$(</span>dpkg --print-architecture<span class="k">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable&#34;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install Docker CE and containerd</span>
</span></span><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/02-install-docker-ce-wsl2.png"
         alt="Installing Docker CE and containerd in Ubuntu WSL2"/> <figcaption>
            <p>Configuring the official Docker CE repository and installing containerd and the Docker daemon inside Ubuntu WSL2.</p>
        </figcaption>
</figure>

<p>Verify that both the Docker daemon and containerd services are running cleanly under systemd:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl status docker
</span></span><span class="line"><span class="cl">systemctl status containerd
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/03-docker-containerd-service-status.png"
         alt="Verifying systemd status for Docker and containerd"/> <figcaption>
            <p>Confirming active (running) status for both docker.service and containerd.service under WSL2 systemd.</p>
        </figcaption>
</figure>

<p>Before moving to Kubernetes, sanity check that native Docker has full GPU access through the WSL2 NVIDIA container runtime:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker container run --gpus all --rm nvidia/cuda:13.3.1-base-ubuntu26.04 nvidia-smi -L
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/04-docker-gpu-passthrough-verification.png"
         alt="Docker container GPU passthrough test"/> <figcaption>
            <p>Direct GPU passthrough test: Docker successfully identifies GPU 0 as the NVIDIA GeForce RTX 4070 Laptop GPU.</p>
        </figcaption>
</figure>

<hr>
<h2 id="run-minikube-with-nvidia-gpu-support-on-wsl2">Run Minikube with NVIDIA GPU Support on WSL2</h2>
<p>Install the latest Minikube Debian package:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
</span></span><span class="line"><span class="cl">sudo dpkg -i minikube_latest_amd64.deb
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/05-install-minikube-deb.png"
         alt="Installing Minikube package in WSL2"/> <figcaption>
            <p>Installing Minikube v1.39.0 via Debian package.</p>
        </figcaption>
</figure>

<p>Following the official <a href="https://minikube.sigs.k8s.io/docs/tutorials/nvidia/">Minikube GPU documentation</a>, starting Minikube with GPU acceleration requires specific driver and runtime parameters. Now start Minikube. Two flags matter here and both are easy to miss:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">minikube start <span class="se">\
</span></span></span><span class="line"><span class="cl">  --driver<span class="o">=</span>docker <span class="se">\
</span></span></span><span class="line"><span class="cl">  --container-runtime<span class="o">=</span>docker <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gpus<span class="o">=</span>all <span class="se">\
</span></span></span><span class="line"><span class="cl">  --memory<span class="o">=</span>12g <span class="se">\
</span></span></span><span class="line"><span class="cl">  --cpus<span class="o">=</span><span class="m">10</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --extra-config<span class="o">=</span>apiserver.service-account-issuer<span class="o">=</span>https://kubernetes.default.svc <span class="se">\
</span></span></span><span class="line"><span class="cl">  --extra-config<span class="o">=</span>apiserver.service-account-signing-key-file<span class="o">=</span>/var/lib/minikube/certs/sa.key
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/06-minikube-start-gpu-flags.png"
         alt="Starting Minikube with GPU support and Docker runtime"/> <figcaption>
            <p>Minikube starting Kubernetes v1.37.0 on Docker, automatically detecting and enabling the nvidia-device-plugin addon.</p>
        </figcaption>
</figure>

<blockquote>
<p>[!IMPORTANT]
<strong>Why <code>--container-runtime=docker</code> is required:</strong>
By default, even when using <code>--driver=docker</code>, Minikube configures containerd as its internal in-node runtime. However, Minikube&rsquo;s <code>--gpus=all</code> flag currently hooks into Docker&rsquo;s OCI runtime wrapper. If you omit <code>--container-runtime=docker</code>, Minikube fails during preflight checks with an invalid flag combination error.</p>
</blockquote>
<p>Notice the startup output: Minikube automatically detects the GPU and enables the <code>nvidia-device-plugin</code> addon for you.</p>
<hr>
<h2 id="verifying-the-cluster-and-gpu-allocation">Verifying the Cluster and GPU Allocation</h2>
<p>Install <code>kubectl</code> to talk to the cluster:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -LO <span class="s2">&#34;https://dl.k8s.io/release/</span><span class="k">$(</span>curl -L -s https://dl.k8s.io/release/stable.txt<span class="k">)</span><span class="s2">/bin/linux/amd64/kubectl&#34;</span>
</span></span><span class="line"><span class="cl">sudo install -o root -g root -m <span class="m">0755</span> kubectl /usr/local/bin/kubectl
</span></span><span class="line"><span class="cl">kubectl version --client
</span></span><span class="line"><span class="cl">kubectl get nodes
</span></span><span class="line"><span class="cl">kubectl get pods -A
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/07-kubectl-install-and-cluster-verification.png"
         alt="Kubectl verification and nvidia-device-plugin DaemonSet"/> <figcaption>
            <p>Node minikube is Ready (v1.37.0) and the nvidia-device-plugin-daemonset is 1/1 Running in kube-system.</p>
        </figcaption>
</figure>

<p>Next, inspect the node&rsquo;s schedulable capacity:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl describe node minikube <span class="p">|</span> grep -A7 <span class="s2">&#34;Capacity:\|Allocatable:&#34;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/08-minikube-node-gpu-capacity.png"
         alt="Minikube node capacity showing GPU resource"/> <figcaption>
            <p>Cluster verification: nvidia.com/gpu: 1 is now officially registered in Capacity and Allocatable.</p>
        </figcaption>
</figure>

<p>A quick test pod confirms the GPU is reachable from inside Kubernetes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: Pod
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: gpu-test
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s">  restartPolicy: Never
</span></span></span><span class="line"><span class="cl"><span class="s">  containers:
</span></span></span><span class="line"><span class="cl"><span class="s">    - name: cuda-test
</span></span></span><span class="line"><span class="cl"><span class="s">      image: nvidia/cuda:13.3.1-base-ubuntu26.04
</span></span></span><span class="line"><span class="cl"><span class="s">      command: [&#34;nvidia-smi&#34;, &#34;-L&#34;]
</span></span></span><span class="line"><span class="cl"><span class="s">      resources:
</span></span></span><span class="line"><span class="cl"><span class="s">        limits:
</span></span></span><span class="line"><span class="cl"><span class="s">          nvidia.com/gpu: 1
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Check the test pod logs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl logs gpu-test
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/09-k8s-cuda-gpu-test-pod.png"
         alt="CUDA test pod running on Minikube"/> <figcaption>
            <p>Test pod running nvidia-smi inside the Kubernetes cluster and successfully accessing the RTX 4070.</p>
        </figcaption>
</figure>

<p>Real GPU scheduling and <code>nvidia.com/gpu</code> resource accounting, without Docker Desktop getting in the way.</p>
<hr>
<h2 id="deploy-vllm-on-kubernetes-with-nvidia-gpu">Deploy vLLM on Kubernetes with NVIDIA GPU</h2>
<p>Model choice for 8 GB VRAM: <strong>Qwen3.5-0.8B</strong> (<code>Qwen/Qwen3.5-0.8B</code>). Small enough to leave real headroom for vLLM&rsquo;s KV cache, which is the main reason to use vLLM over llama.cpp or Ollama in the first place, and it is a capable model at that size. As detailed in the <a href="https://docs.vllm.ai/">vLLM documentation</a>, the server exposes an OpenAI-compatible API endpoint over HTTP.</p>
<h3 id="the-shared-memory-devshm-gotcha">The Shared Memory (<code>/dev/shm</code>) Gotcha</h3>
<p>When running vLLM via the Docker CLI, passing <code>--ipc=host</code> lets workers exchange tensors and state across processes using host shared memory. In Kubernetes, pods do not share the host IPC namespace by default, and Kubernetes provisions <code>/dev/shm</code> as a minimal 64 MB tmpfs mount.</p>
<p>Some PyTorch and vLLM multiprocessing workloads can require significantly more shared memory than Kubernetes&rsquo; default <code>/dev/shm</code> (which defaults to a minimal 64 MB tmpfs mount). If insufficient shared memory is available, initialization or worker processes may fail. The Kubernetes solution is mounting an <code>emptyDir</code> volume backed by host RAM (<code>medium: Memory</code>) with a dedicated <code>sizeLimit</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vllm-qwen-k8s.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vllm/vllm-openai:v0.28.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--model=Qwen/Qwen3.5-0.8B&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--gpu-memory-utilization=0.7&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--max-model-len=8192&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">nvidia.com/gpu</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hf-cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/root/.cache/huggingface</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dshm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/shm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hf-cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">emptyDir</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dshm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">emptyDir</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">medium</span><span class="p">:</span><span class="w"> </span><span class="l">Memory</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">sizeLimit</span><span class="p">:</span><span class="w"> </span><span class="l">2Gi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIP</span><span class="w">
</span></span></span></code></pre></div><p>Two practical notes on this manifest:</p>
<ul>
<li><strong>Image version pinning:</strong> I pin the image version (<code>v0.28.0</code>) here so the deployment remains reproducible. You can update the image tag after validating compatibility with your CUDA and model version.</li>
<li><strong>Hugging Face cache:</strong> <code>emptyDir</code> is used here for simplicity. The Hugging Face model cache is lost when the pod is recreated. For repeated testing or larger models, consider using a PersistentVolumeClaim (PVC) or a host-mounted path.</li>
</ul>
<p>Apply the manifest and watch the deployment roll out:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f vllm-qwen-k8s.yaml
</span></span><span class="line"><span class="cl">kubectl get pods,svc
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/10-vllm-deployment-and-service-running.png"
         alt="vLLM deployment and service running"/> <figcaption>
            <p>Applying vllm-qwen-k8s.yaml and confirming the vLLM pod reaches 1/1 Running status alongside the ClusterIP service.</p>
        </figcaption>
</figure>

<p>Stream the pod logs to inspect the vLLM initialization sequence:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl logs vllm-qwen-5c8bbc786f-gdpmj -f
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/11-vllm-pod-startup-logs.png"
         alt="vLLM pod engine initialization logs"/> <figcaption>
            <p>vLLM v0.28.0 engine initialization: resolving Qwen3_5ForConditionalGeneration, setting max model length to 8192, and warming up CUDA graphs.</p>
        </figcaption>
</figure>

<p>Once PyTorch compilation and CUDA graph capture finish, the ASGI application completes startup and begins listening on port 8000:</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/12-vllm-server-ready-and-metrics.png"
         alt="vLLM application startup complete and request logs"/> <figcaption>
            <p>vLLM application startup complete: exposing OpenAI-compatible endpoints (/v1/chat/completions) with live throughput metrics.</p>
        </figcaption>
</figure>

<hr>
<h2 id="testing-inference-and-hardware-telemetry">Testing Inference and Hardware Telemetry</h2>
<p>Forward port 8000 from the cluster service to the local machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl port-forward svc/vllm-qwen 8000:8000
</span></span></code></pre></div><p>Now issue an OpenAI-standard chat completion request using <code>curl</code>:</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:8000/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <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;Qwen/Qwen3.5-0.8B&#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;Say hello in one sentence.&#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/vllm-wsl2-minikube/13-vllm-curl-chat-completion-response.png"
         alt="Calling vLLM chat completions API with curl"/> <figcaption>
            <p>Inference test: 48 tokens generated cleanly through the OpenAI-compatible vLLM API running in Minikube.</p>
        </figcaption>
</figure>

<p>The response returned without issues, with server logs showing generation throughput averaging 23.5 tokens/s.</p>
<p>Finally, checking Windows Task Manager confirms the hardware utilization on the host:</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/14-windows-task-manager-gpu-vram-usage.png"
         alt="Windows Task Manager GPU memory usage"/> <figcaption>
            <p>Windows Task Manager telemetry: 4.4 GB of 8.0 GB dedicated VRAM allocated to the WSL2 vLLM pod at 45°C.</p>
        </figcaption>
</figure>

<p>Dedicated GPU memory held at <strong>4.4 / 8.0 GB</strong> (in line with the <code>--gpu-memory-utilization 0.7</code> setting plus desktop display overhead), with the GPU temperature at 45°C.</p>
<hr>
<h2 id="beyond-local-dev-why-production-clusters-use-the-nvidia-gpu-operator">Beyond Local Dev: Why Production Clusters Use the NVIDIA GPU Operator</h2>
<p>For a single-node laptop or a Minikube sandbox, managing drivers on the host and running a standalone device plugin DaemonSet gets the job done.</p>
<p>In production Kubernetes clusters, manual node configuration does not scale. When you operate multi-node clusters across cloud providers or bare metal, nodes get provisioned dynamically by autoscalers, Linux kernel patch levels drift, and different GPU architectures (such as A100, H100, or L40S) coexist in the same cluster.</p>
<p>This is why production setups deploy the <a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/">NVIDIA GPU Operator</a> instead of managing the device plugin directly.</p>
<p>The GPU Operator automates the deployment and lifecycle management of the NVIDIA software stack required for GPU-enabled Kubernetes nodes. Driven by the <code>ClusterPolicy</code> Custom Resource Definition, it manages several components depending on your cluster configuration:</p>
<ol>
<li><strong>Driver Management:</strong> Depending on the operating environment, the Operator can compile and load the NVIDIA driver container matched to the node kernel, or use pre-installed host drivers (common in managed Kubernetes environments like GKE, EKS, or AKS).</li>
<li><strong>Container Toolkit and CDI Configuration:</strong> Depending on the Operator version and runtime configuration, GPU injection can be configured through direct NVIDIA runtime integration or CDI-based device injection (Container Device Interface), which modern releases enable by default.</li>
<li><strong>Device Plugin Supervision:</strong> It deploys and manages the lifecycle of the NVIDIA Kubernetes Device Plugin DaemonSet, keeping it in sync with driver and runtime availability.</li>
<li><strong>Node Feature Discovery (GFD):</strong> It automatically detects physical GPU capabilities and labels each node (such as <code>nvidia.com/gpu.product: NVIDIA-A100-SXM4-80GB</code> or <code>nvidia.com/gpu.family: ampere</code>). Workloads can then target specific GPU models using standard Kubernetes <code>nodeSelector</code> or affinity rules.</li>
<li><strong>Cluster Metrics with DCGM Exporter:</strong> It runs the Data Center GPU Manager (DCGM) exporter to collect metrics (GPU compute utilization, VRAM usage, temperature, power draw, and memory bandwidth) and expose them to Prometheus for cluster monitoring and alerting.</li>
<li><strong>MIG Management:</strong> On enterprise GPUs that support Multi-Instance GPU (MIG), the Operator can dynamically partition a single physical GPU into isolated hardware instances, allowing smaller workloads to share an A100 or H100 without memory interference.</li>
</ol>
<p>In short: the device plugin provides basic GPU scheduling for pods. The GPU Operator manages the operational lifecycle, monitoring, and driver stack required to run GPU infrastructure reliably in production.</p>
<hr>
<h2 id="key-takeaways">Key Takeaways</h2>
<ol>
<li><strong>Docker Desktop Kubernetes isolates nodes without GPU passthrough:</strong> In tested setups, <code>desktop-control-plane</code> runs in a nested <code>sysbox-runc</code> sandbox without GPU device passthrough flags.</li>
<li><strong>Minikube on native Docker CE in WSL2 works reliably:</strong> Launching Minikube with <code>--driver=docker --container-runtime=docker --gpus=all</code> automatically activates the NVIDIA device plugin addon.</li>
<li><strong>Understand device plugin vs node prerequisites:</strong> The device plugin only manages discovery and Kubelet allocation. Host drivers, Container Toolkit, and runtime configuration must already exist on the worker node.</li>
<li><strong>Always size <code>/dev/shm</code> in Kubernetes:</strong> Multiprocessing in PyTorch and vLLM quickly exceeds the default 64 MB tmpfs mount. Mount an <code>emptyDir</code> memory volume backed by host RAM.</li>
<li><strong>Budget VRAM carefully:</strong> Setting <code>--gpu-memory-utilization 0.7</code> on a compact model like Qwen3.5-0.8B preserves headroom for the KV cache on an 8 GB GPU.</li>
<li><strong>Deploy the NVIDIA GPU Operator in production:</strong> Production multi-node clusters rely on the GPU Operator to manage drivers, CDI device injection, node feature discovery, and DCGM monitoring.</li>
</ol>
<hr>
<p>Thanks for reading! If you run into issues setting up GPU passthrough on your machine, have questions, or have suggestions for other local AI setups to explore, leave a comment below.</p>
]]></content:encoded></item><item><title>Running a 35B MoE Model on an 8 GB Laptop GPU: Testing FreeToken</title><link>https://vishctl.dev/posts/running-freetoken-on-8gb-laptop-gpu/</link><pubDate>Mon, 07 Sep 2026 14:30:00 +0800</pubDate><guid>https://vishctl.dev/posts/running-freetoken-on-8gb-laptop-gpu/</guid><category>ai</category><category>local-llm</category><category>freetoken</category><category>gpu</category><category>homelab</category><category>nvidia</category><description>Testing FreeToken on an RTX 4070 Laptop GPU: running Qwen 3.6 35B-A3B NVFP4 across 8 GB VRAM and 32 GB RAM with hierarchical memory offloading.</description><content:encoded><![CDATA[<p>Running large language models locally usually comes down to a hard hardware boundary: video RAM. If a model does not fit into your GPU&rsquo;s VRAM, performance usually falls off a cliff as soon as standard runtimes fall back to system memory over the PCIe bus.</p>
<p>Mixture of Experts (MoE) architectures offer an attractive theoretical escape hatch. While the total parameter count can be large (30B to 70B+), only a sparse subset of expert layers activates for any given token. However, standard local runtimes still require loading the entire weight footprint into memory, which puts 30B+ models out of reach for everyday consumer laptops with 8 GB of VRAM.</p>
<p>Enter <a href="https://github.com/FlashML-org/FreeToken">FreeToken</a> by FlashML, backed by their research paper (<a href="https://arxiv.org/abs/2608.16157">arXiv:2608.16157</a>). FreeToken is an edge inference runtime designed specifically to run frontier MoE models on consumer hardware by dynamically managing a hierarchical cache between GPU VRAM and host RAM.</p>
<p>I recently downloaded FreeToken to put their claims to the test on my modest laptop GPU. Here is what the setup looked like, how FreeToken handles memory under the hood, and the real-world generation numbers I observed.</p>
<h3 id="the-test-rig">The Test Rig</h3>
<p>My test machine is a portable laptop, not a high-end multi-GPU workstation:</p>
<ul>
<li><strong>CPU:</strong> 13th Gen Intel Core i9-13900H (14 cores, 20 threads)</li>
<li><strong>GPU:</strong> NVIDIA GeForce RTX 4070 Laptop GPU (8.0 GiB VRAM)</li>
<li><strong>Host RAM:</strong> 32 GB DDR5 (31.7 GiB usable)</li>
<li><strong>Operating System:</strong> Windows 11</li>
<li><strong>Runtime:</strong> FreeToken Desktop (v0.2.0-beta.17)</li>
</ul>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/01-freetoken-website-download.png"
         alt="FreeToken website download page"/> <figcaption>
            <p>The FreeToken landing page highlights bringing frontier models to consumer edge hardware.</p>
        </figcaption>
</figure>

<p>On paper, an 8 GB VRAM budget makes running a 35-billion parameter model look impossible. In traditional setups, 8 GB VRAM limits you to 7B or 8B parameter models in 4-bit quantizations (such as Q4_K_M). Attempting to load a 35B model typically triggers out-of-memory errors or slows inference to a crawl.</p>
<h3 id="installing-freetoken-and-exploring-the-library">Installing FreeToken and Exploring the Library</h3>
<p>The FreeToken desktop app is available to download directly from the <a href="https://www.flashml.ai/">FlashML website</a>. It provides a self-contained installer that is quick to set up on Windows. When you launch it, the interface automatically detects your hardware specs, available VRAM, and system RAM, and presents a curated library of models optimized for edge offloading.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/02-hardware-specs-and-model-library.png"
         alt="FreeToken hardware detection and model library"/> <figcaption>
            <p>FreeToken accurately detects the RTX 4070 Laptop GPU (8 GB) and 32 GB RAM, recommending compatible models.</p>
        </figcaption>
</figure>

<p>For this test, I selected <code>Qwen3.6-35B-A3B NVFP4</code> (<code>nvidia/Qwen3.6-35B-A3B-NVFP4</code>). This is a 35-billion parameter MoE model using NVIDIA&rsquo;s 4-bit floating point (NVFP4) format, with an initial download size of 21.9 GiB.</p>
<p>Before loading the model, the console dashboard showed clean baseline resource usage:</p>
<ul>
<li><strong>GPU VRAM:</strong> 0.4 / 8.0 GiB</li>
<li><strong>Host RAM:</strong> 11.0 / 31.7 GiB</li>
<li><strong>GPU Temp:</strong> 42°C at idle</li>
</ul>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/03-system-resources-idle.png"
         alt="System resources at idle before model loading"/> <figcaption>
            <p>Idle console telemetry: 0.4 GiB VRAM used, ready for model weights.</p>
        </figcaption>
</figure>

<h3 id="weight-conversion-the-ftw-format">Weight Conversion: The FTW Format</h3>
<p>Once the 21.9 GiB download finished, FreeToken flagged the model as needing conversion before it could be launched.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/05-download-complete-needs-conversion.png"
         alt="Download complete prompt indicating conversion is required"/> <figcaption>
            <p>Raw model download complete (21.9 GiB), prompting for weight conversion.</p>
        </figcaption>
</figure>

<p>FreeToken converts raw Hugging Face weights into its proprietary format called <strong>FTW</strong> (FreeToken Weight format). During this process, the engine repacks and organizes the tensors into memory-mapped structures optimized for rapid streaming between host RAM and GPU memory.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/06-converting-weights-to-ftw.png"
         alt="Converting raw weights into FTW format"/> <figcaption>
            <p>Repacking raw weights into FTW format for fast memory streaming.</p>
        </figcaption>
</figure>

<p>The conversion took just over a minute on the i9-13900H and NVMe storage, producing a compact 19.5 GiB weight package.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/07-conversion-complete-ftw.png"
         alt="Conversion complete showing 19.5 GiB repacked size"/> <figcaption>
            <p>Conversion complete: 19.5 GiB ready for execution.</p>
        </figcaption>
</figure>

<h3 id="loading-the-model-and-memory-allocation">Loading the Model and Memory Allocation</h3>
<p>Starting the model triggers the weight allocation phase. Instead of attempting to cram the entire 19.5 GiB into the 8 GB VRAM, FreeToken partitions the workload across both memory tiers.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/08-loading-weights-into-vram.png"
         alt="Loading model weights into VRAM"/> <figcaption>
            <p>Starting model and populating active weights into VRAM.</p>
        </figcaption>
</figure>

<p>Once loaded, the memory profile was eye-opening:</p>
<ul>
<li><strong>VRAM Usage:</strong> 6.8 GiB / 8.0 GiB (85% utilization, leaving a healthy buffer for OS display compositing)</li>
<li><strong>Host RAM Usage:</strong> 30.2 GiB / 31.7 GiB (95% utilization)</li>
<li><strong>GPU Status:</strong> Running at 45°C, drawing only 7W at idle</li>
</ul>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/09-model-running-vram-ram-usage.png"
         alt="Model running with VRAM and RAM utilization"/> <figcaption>
            <p>Model running: 6.8 GiB in VRAM and 30.2 GiB in system RAM.</p>
        </figcaption>
</figure>

<h3 id="the-cache-architecture-under-the-hood">The Cache Architecture Under the Hood</h3>
<p>The console view provides detailed insight into how FreeToken manages this memory footprint:</p>
<ol>
<li><strong>MoE Expert Cache:</strong> FreeToken allocated 916 active expert slots in VRAM out of a total pool of 10,240 slots, consuming 1.51 GiB of VRAM. As different experts are needed during generation, they are dynamically paged in and out from the host RAM cache.</li>
<li><strong>KV Cache:</strong> 8K tokens of context were allocated in VRAM, consuming just 0.16 GiB.</li>
<li><strong>Mamba State Slots:</strong> 24 state slots were reserved, using 1.44 GiB of VRAM.</li>
</ol>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/10-cache-architecture-console.png"
         alt="Cache architecture telemetry in console"/> <figcaption>
            <p>Detailed cache breakdown: MoE expert cache, KV cache, and Mamba state slots in VRAM.</p>
        </figcaption>
</figure>

<p>This hierarchical design is the core reason the model can run without crashing. Inactive experts reside in system RAM (filling ~30 GB of system memory), while the active attention layers, KV cache, and currently triggered experts reside in the 8 GB VRAM.</p>
<h3 id="real-world-performance--generation-speed">Real-World Performance &amp; Generation Speed</h3>
<p>To evaluate real-world performance, I tested a conversational prompt with reasoning enabled:</p>
<blockquote>
<p><strong>User:</strong> &ldquo;hi, how are you ?&rdquo;</p>
</blockquote>
<p>The model engaged its full thinking process, taking 1 minute and 9 seconds to plan and structure its response, and then generated 393 tokens at <strong>4.3 tokens per second</strong>.</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/11-chat-inference-test.png"
         alt="Chat inference test output and token speed"/> <figcaption>
            <p>Chat generation benchmark: 393 tokens generated at 4.3 tok/s with full chain-of-thought thinking.</p>
        </figcaption>
</figure>

<p>While 4.3 tokens per second is not real-time voice conversational speed, it is remarkably steady for running a <strong>35-billion parameter model on an 8 GB laptop GPU</strong>. For tasks like code review, background agent execution, document analysis, or local drafting, 4.3 tok/s is completely practical.</p>
<p>The model detail sheet confirms the architecture and licensing details:</p>
<ul>
<li><strong>Base Repository:</strong> <code>nvidia/Qwen3.6-35B-A3B-NVFP4</code></li>
<li><strong>Context Length:</strong> 262,144 tokens (256K context window)</li>
<li><strong>License:</strong> Apache 2.0</li>
</ul>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/12-model-details-huggingface.png"
         alt="Model detail card on FreeToken"/> <figcaption>
            <p>Model details: 262K context window, NVFP4 quantization, and Apache 2.0 license.</p>
        </figcaption>
</figure>

<h3 id="built-in-agentic-tooling--local-endpoints">Built-in Agentic Tooling &amp; Local Endpoints</h3>
<p>FreeToken also includes built-in support for developer tools and coding agents.</p>
<p>The <strong>Apps</strong> tab exposes both OpenAI-compatible and Anthropic-compatible local HTTP endpoints:</p>
<ul>
<li><strong>OpenAI Endpoint:</strong> <code>http://127.0.0.1:1919/v1</code></li>
<li><strong>Anthropic Messages Endpoint:</strong> <code>http://127.0.0.1:1919/v1/messages</code></li>
</ul>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/freetoken-8gb-laptop-gpu/04-local-api-endpoints-and-apps.png"
         alt="FreeToken Apps tab showing local endpoints and integrations"/> <figcaption>
            <p>Local OpenAI and Anthropic compatible endpoints with one-click configurations for Claude Code, Codex, opencode, and more.</p>
        </figcaption>
</figure>

<p>FreeToken provides quick configuration presets for popular coding assistants:</p>
<ul>
<li><strong>Claude Code:</strong> Configurable via local Anthropic endpoint routing</li>
<li><strong>Codex / opencode / openclaw:</strong> One-click environment setups</li>
<li><strong>Hermes / DeepSeek Harness:</strong> Direct command-line integration</li>
</ul>
<p>Having drop-in compatibility with both OpenAI and Anthropic API schemas means you can point tools like Claude Code directly to your localhost port without running a reverse proxy.</p>
<h3 id="where-freetoken-fits">Where FreeToken Fits</h3>
<p>FreeToken is a relatively young product, but its approach solves a real constraint for local inference.</p>
<h4 id="strengths">Strengths</h4>
<ul>
<li><strong>Bypasses the VRAM limit:</strong> Running a 35B MoE model on an 8 GB laptop GPU without manual layer-by-layer offload tuning works out of the box.</li>
<li><strong>Hierarchical caching:</strong> The dynamic MoE expert cache keeps VRAM usage around 6.8 GiB, preventing driver resets and CUDA out-of-memory crashes.</li>
<li><strong>Local developer endpoints:</strong> Dual support for OpenAI and Anthropic endpoints makes integration with modern coding agents straightforward.</li>
<li><strong>Simple setup on Windows:</strong> No manual compilation, no CUDA toolkit troubleshooting, and a clean desktop GUI.</li>
</ul>
<h4 id="limitations">Limitations</h4>
<ul>
<li><strong>System RAM is the real requirement:</strong> While VRAM requirements drop significantly, your host RAM must be large enough to hold the repacked weights. On my machine, the model took 30.2 GB of system RAM. If your laptop only has 16 GB of RAM, you will not be able to run 35B models this way.</li>
<li><strong>Initial conversion step:</strong> Each new model requires a one-time repacking step into the FTW format before the first run.</li>
<li><strong>Generation throughput:</strong> At roughly 4.3 tokens per second, it is well suited for asynchronous workloads, coding agents, and complex reasoning queries, but not for instant conversational back-and-forth.</li>
</ul>
<h3 id="wrap-up">Wrap-up</h3>
<p>If you have a laptop with 32 GB of system RAM and a modest 8 GB NVIDIA GPU, FreeToken is worth checking out. It makes 35B-class MoE models runnable on consumer hardware that would otherwise choke on them, keeping your data completely local.</p>
<p>You can download the desktop app directly from <a href="https://www.flashml.ai/">FlashML</a>, check out the project on <a href="https://github.com/FlashML-org/FreeToken">GitHub</a>, and read their research paper on <a href="https://arxiv.org/abs/2608.16157">arXiv:2608.16157</a>.</p>
]]></content:encoded></item></channel></rss>