{"id":1097,"date":"2026-09-14T09:30:00","date_gmt":"2026-09-13T23:30:00","guid":{"rendered":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/"},"modified":"2026-11-03T20:27:58","modified_gmt":"2026-11-03T09:27:58","slug":"smart-routing-single-gpu-local-llm","status":"publish","type":"post","link":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/","title":{"rendered":"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11)"},"content":{"rendered":"\n<p class=\"has-pale-cyan-blue-background-color has-background wp-block-paragraph\">Our first smart router made every request slower and pushed other clients&#8217; models out of memory. This is how we rebuilt it in the lab, with rules before models, resident models first and clear refusals, and how to build one yourself.<\/p>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Where this fits \u00b7 Running Local AI in Production \u00b7 Part 5 of 11<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:17px\">You are reading Part 5 of 11 in a series where we built a private AI server in our lab and ran realistic test workloads on it for seven weeks. Each part stands on its own. The previous part covered fitting every prompt to a local model&#8217;s context window; this one covers routing requests across models on a single GPU without constant model swaps.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">\u2190 Part 4: What Happens When a Prompt Is Too Big for a Local LLM?<\/a> &nbsp;\u00b7&nbsp; <a href=\"https:\/\/catharsis.net.au\/blog\/running-local-ai-in-production-series-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Series guide<\/a> &nbsp;\u00b7&nbsp; <a href=\"https:\/\/catharsis.net.au\/blog\/gitops-for-a-gpu-ai-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 6: How Do You Keep an AI Server Stable? GitOps for Local AI \u2192<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\">Just want it working? Follow the <a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">step-by-step setup guide<\/a>: local AI with Ollama and LiteLLM, from a bare server to a secure gateway.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See all 11 parts of the series<\/summary>\n\n<ol class=\"wp-block-list\">\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Local AI or Cloud AI? Why We Built a Private AI Server<\/a>: why we built the lab on our own hardware, what ran on it and what that choice cost<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Control Access to Local AI Models? Use a Gateway<\/a>: one LLM gateway as the only way to reach a model: keys, budgets and tracing<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Why Does a Local AI Server Freeze? Sizing Memory for LLMs<\/a>: five hard freezes and the memory budget that ended them<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Happens When a Prompt Is Too Big for a Local LLM?<\/a>: fitting every prompt to a local model&#8217;s context window<\/li>\n\n\n<li><strong>How Do You Run Several AI Models on One GPU Without Slowdowns?<\/strong> (you are here): routing requests across models on a single GPU without constant model swaps<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/gitops-for-a-gpu-ai-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Keep an AI Server Stable? GitOps for Local AI<\/a>: running the server from a Git repository: deploys, drift, pinning and safe AI agents<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/observability-for-local-ai\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Is Your Local AI Server Actually Doing? Observability Basics<\/a>: making the server observable: traces, dashboards, usage and heat<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/multi-agent-stock-analysis-local-llm\/\" target=\"_blank\" rel=\"noreferrer noopener\">Can Local AI Agents Analyse Stocks? How We Tested and Scored Them<\/a>: a multi-agent stock-analysis experiment and how we scored the agents<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/telegram-ai-agent-local-llms\/\" target=\"_blank\" rel=\"noreferrer noopener\">Why Was Our Telegram AI Agent So Slow? The Hidden Prompt Cost<\/a>: a Telegram AI agent on local models, and why its fixed prompt became the budget<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-business-workflows\/\" target=\"_blank\" rel=\"noreferrer noopener\">Can AI Automate Business Workflows Without Sending Data Out?<\/a>: document, email and knowledge-base workflows that keep data on the server<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/private-ai-lab-lessons-and-whats-next\/\" target=\"_blank\" rel=\"noreferrer noopener\">Is Private AI Worth It? Lessons From Seven Weeks in Our Lab<\/a>: the lessons from seven weeks, the honest limits and what comes next<\/li>\n\n<\/ol>\n\n<\/details>\n\n<\/div><\/div>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><em>This series documents a private AI server we built and stress-tested in our lab. Workloads and data are synthetic or generalised; the problems, measurements and fixes are real.<\/em><\/p>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">TL;DR<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Keep a small set of models loaded and route between them without ever forcing a swap. In our lab two models stay resident, plain rules pick between them, and routing a request takes 0.4 to 0.5 seconds.<\/li>\n\n\n<li>Test clients ask for a model called <code>auto<\/code> and a small router picks the local model. Its first version made every <code>auto<\/code> call take 9 seconds instead of 2, and pushed other clients&#8217; models out of memory.<\/li>\n\n\n<li>We rebuilt it to decide most requests with plain rules, run its tiny classifier on a separate instance, and keep exactly two big models loaded.<\/li>\n\n\n<li>The router now prefers a model that is already loaded, never picks a cloud model by itself, and refuses clearly when nothing fits.<\/li>\n\n\n<li>The lesson: on one GPU, the expensive part of routing is a model swap, not the decision.<\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Jargon buster: 15 terms used in this post, explained in plain English<\/summary>\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Unified memory<\/strong>: A design where the CPU and GPU share one pool of memory instead of the GPU having its own. Flexible, but both compete for the same space.<\/li>\n\n\n<li><strong>Slot (parallel requests)<\/strong>: One request a model server can work on at the same time. More slots serve more people at once but reserve more memory.<\/li>\n\n\n<li><strong>Model swap<\/strong>: Unloading one model from memory and loading another. It takes seconds to minutes, so frequent swapping makes everything slow.<\/li>\n\n\n<li><strong>Ollama<\/strong>: Free, open-source software that downloads and runs AI models on your own machine and answers requests over a local API.<\/li>\n\n\n<li><strong>LiteLLM<\/strong>: Open-source software that puts one front door in front of many AI models, with per-user keys, budgets, limits and logging.<\/li>\n\n\n<li><strong>LLM gateway<\/strong>: A single service every AI request must pass through, so access, budgets and logging are controlled in one place.<\/li>\n\n\n<li><strong>API key \/ virtual key<\/strong>: A secret string that identifies the program making a request. Giving each program its own key lets you limit and track it separately.<\/li>\n\n\n<li><strong>Rate limit<\/strong>: A cap on how many requests a key can make in a given time, so one program can&#8217;t swamp the server.<\/li>\n\n\n<li><strong>Open WebUI<\/strong>: An open-source, browser-based chat interface, similar to ChatGPT, that can talk to models running on your own server.<\/li>\n\n\n<li><strong>Langfuse<\/strong>: Open-source software that records every AI request (prompt, answer, timing and cost) so you can see what happened and why.<\/li>\n\n\n<li><strong>Trace<\/strong>: A detailed record of one request as it passes through each part of a system, used to find where time or errors come from.<\/li>\n\n\n<li><strong>Telegram bot<\/strong>: An automated account on the Telegram messaging app that people chat with; here, the front end for an AI agent.<\/li>\n\n\n<li><strong>Configuration drift<\/strong>: When the live system slowly stops matching its recorded configuration because of manual or untracked changes.<\/li>\n\n\n<li><strong>Sandbox<\/strong>: An isolated environment that limits what a program can see or change, so a misbehaving program can&#8217;t affect the rest of the system.<\/li>\n\n\n<li><strong>HTTP status codes (400, 413, 429)<\/strong>: Standard codes a server returns: 400 means a bad request, 413 means it&#8217;s too large, 429 means too many requests.<\/li>\n\n<\/ul>\n\n<\/details>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">In this post<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><a href=\"#the-problem-why-did-routing-make-everything-slower\">The problem: why did routing make everything slower?<\/a><\/li>\n\n\n<li><a href=\"#what-we-were-aiming-for\">What we were aiming for<\/a><\/li>\n\n\n<li><a href=\"#how-we-approached-it\">How we approached it<\/a><\/li>\n\n\n<li><a href=\"#what-we-built\">What we built<\/a><\/li>\n\n\n<li><a href=\"#challenges-we-hit\">Challenges we hit<\/a><\/li>\n\n\n<li><a href=\"#the-result\">The result<\/a><\/li>\n\n\n<li><a href=\"#do-it-yourself-rules-first-routing-with-a-small-resident-set\">Do it yourself: rules-first routing with a small resident set<\/a><\/li>\n\n\n<li><a href=\"#key-takeaways\">Key takeaways<\/a><\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-problem-why-did-routing-make-everything-slower\">The problem: why did routing make everything slower?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The lab server is the NVIDIA DGX Spark from <a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 1<\/a>, with roughly 120 GB of usable <strong>unified memory<\/strong>, one pool shared by the CPU and GPU. As <a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 3<\/a> explained, every model loaded into that pool reserves memory for its context, and loading a different model means pushing another one out. On one GPU, a model swap costs seconds, and a cold load can take more than a minute.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Many different callers share the server. IDE plugins send code, test users ask questions in Open WebUI, and a Telegram agent (<a href=\"https:\/\/catharsis.net.au\/blog\/telegram-ai-agent-local-llms\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 9<\/a>) sends long prompts full of tool definitions. None of them should need to know which model is loaded, so the gateway (LiteLLM, <a href=\"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 2<\/a>) offers a model called <code>auto<\/code>, and behind it sits a small service we call the smart router.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first router was a FastAPI service of about 380 lines. It sent the last three user turns to <code>qwen3:0.6b<\/code>, a 0.6-billion-parameter model, for a one-word label (CODE, AGENTIC, QA or SIMPLE), mapped CODE to qwen3-coder and everything else to gemma4, and forwarded the request. The classifier ran on the main Ollama instance, which at the time held one model at a time. So each <code>auto<\/code> request loaded the classifier, which evicted the big model, and then the big model loaded again. An <code>auto<\/code> request took 9 seconds against 2 seconds calling the model directly, and if another test client was using a different model at that moment, the router evicted theirs too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A review found more. The router kept its own model list, which drifted from the gateway&#8217;s every time a model was added or renamed. It didn&#8217;t know what was loaded or how long the prompt was, so it would send a 30,000-token prompt to a model with a 16,384-token window. And nothing stopped it choosing a paid cloud model. It just happened not to, and one config edit could have changed that.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"what-we-were-aiming-for\">What we were aiming for<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Routing adds well under a second, and never causes a model swap on its own.<\/li>\n\n\n<li>One list of models, kept in the gateway, so the router can&#8217;t drift.<\/li>\n\n\n<li>Cloud, experimental and oversized choices never happen automatically. A prompt that doesn&#8217;t fit gets a clear error, never silent truncation.<\/li>\n\n\n<li>Every routing decision is visible in the traces, so we can check it was right.<\/li>\n\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"how-we-approached-it\">How we approached it<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The obvious fix was a better classifier, but timing showed the classifier itself was fast, 13 ms per call once loaded. The cost was where it ran and what it evicted. So we looked at the problem as memory first and accuracy second.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We considered running the classifier on the CPU, and chose a separate small Ollama instance instead, because it also gives other helper models, such as embedding models, a stable home. We considered letting <code>auto<\/code> choose from five or more models, and chose two, because both can stay loaded and nothing gets evicted. And instead of making the model smarter, we put plain rules in front of it, so the model only sees the cases rules can&#8217;t settle.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"what-we-built\">What we built<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Two resident models and a separate classifier<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The main Ollama instance now holds two models (<code>OLLAMA_MAX_LOADED_MODELS=2<\/code>): <strong>qwen3-coder<\/strong> for code at 20.3 GB and <strong>gemma4<\/strong> for chat, questions and agent work at 17.5 GB, measured together at 37.8 GB with one slot each. Everything else on disk is &quot;cold&quot;: callable by name, never chosen automatically. A separate small Ollama instance on the same pinned version runs only utility models (the server also runs a sandbox instance for a risky model, see <a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 3<\/a>). It holds <code>qwen3:0.6b<\/code> with a 24-hour keep-alive, and its weights are hard-linked from the main instance, so they take no extra disk.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">One model list, read from the gateway<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The router reads the model list from the gateway&#8217;s <code>\/model\/info<\/code> endpoint at start-up and every five minutes, keeping the last good list if a refresh fails. Each model entry carries a few tags:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>model_info:\n  site_tier: warm           # warm | cold | cloud | sandbox (only warm is auto-routable)\n  capabilities: [code, fim, chat]\n  context_window: 32768\n  cost_class: free          # free | metered\n  routable: true            # false = callable only by explicit name\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Adding a model now means adding one gateway entry.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A routing pipeline that does the cheap steps first<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Slash commands win.<\/strong> If the last user message starts with a command, the caller has chosen. An explicit name still goes through the gateway&#8217;s per-key model list and rate limit, so a key can only reach models it is already allowed to call.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>Command<\/th>\n<th>Effect<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>\/code<\/code>, <code>\/qa<\/code>, <code>\/agent<\/code>, <code>\/simple<\/code>, <code>\/fast<\/code>, <code>\/big<\/code><\/td>\n<td>shortcut to a configured model<\/td>\n<\/tr>\n<tr>\n<td><code>\/model:&lt;name&gt; &lt;prompt&gt;<\/code><\/td>\n<td>any model the caller&#8217;s key may use, including cold ones<\/td>\n<\/tr>\n<tr>\n<td><code>!long &lt;prompt&gt;<\/code><\/td>\n<td>raise the answer cap to 16,384 tokens, half of the 32k window<\/td>\n<\/tr>\n<tr>\n<td><code>\/help<\/code>, <code>\/models<\/code><\/td>\n<td>list every model with its tier and whether it is loaded right now<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rules decide the clear cases<\/strong> in effectively zero time. A <code>tools<\/code> array means AGENTIC, because only an agent sends tool definitions. Code fences, stack traces, file paths with extensions, <code>def<\/code>, <code>class<\/code> or <code>import<\/code>, <code>SELECT \u2026 FROM<\/code>, error names such as <code>TypeError:<\/code> and call syntax like <code>df.dropna(...)<\/code> mean CODE. Twelve words or fewer, with no question mark and no request to explain, draft or summarise, means SIMPLE. A question or a prose task such as &quot;explain&quot; or &quot;translate&quot;, with no sign of code, means QA.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The tiny classifier handles the rest<\/strong>, using a prompt with examples and parsing that tolerates near-misses (more on that below).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Labels become candidates.<\/strong> Each label maps to a capability: CODE needs <code>code<\/code>, AGENTIC needs <code>tools<\/code>, QA and SIMPLE need <code>chat<\/code>. A candidate must be routable, in the <code>warm<\/code> tier, have the capability, and be large enough for the estimated prompt plus the answer cap. Cloud, sandbox and cold models are never chosen automatically, even if someone marks one routable by mistake. The estimate is deliberately rough, characters divided by four plus the caller&#8217;s <code>max_tokens<\/code> or the label&#8217;s default cap (1,024 for SIMPLE, 4,096 for QA and AGENTIC, 8,192 for CODE).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Unlike the gateway&#8217;s exact count in Part 4, this only has to pick a model, so being off by 20% costs nothing. The exact count and admission check happen at the gateway (<a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 4<\/a>), which applies its own budget to <code>auto<\/code> so an oversized request is refused at the first hop.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Loaded models come first.<\/strong> The router asks Ollama what is loaded (<code>\/api\/ps<\/code>, cached for five seconds) and puts a loaded candidate first, then breaks ties by a preference order and the larger window. Capability still beats residency: a code request goes to qwen3-coder even if only gemma4 is in memory. If Ollama can&#8217;t be reached, the router treats nothing as loaded.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>When nothing fits, it says so.<\/strong> The router returns HTTP 413 <code>context_too_large<\/code>, naming the smallest model that could take the prompt (callable with <code>\/model:&lt;name&gt;<\/code>) or saying there is none. The gateway&#8217;s own check returns the <code>400<\/code> from Part 4; the two codes come from two different hops.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Every decision in the traces<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each forwarded request carries its decision: the label, the reason (for example <code>rule: language + code noun; CODE \u2192 qwen3-coder (resident)<\/code>), whether a loaded model was used, whether the caller chose, whether a rule or the classifier decided, and the estimated size. In the tracing tool (Langfuse, <a href=\"https:\/\/catharsis.net.au\/blog\/observability-for-local-ai\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 7<\/a>) the router sets filterable metadata, a trace name such as <code>auto \u2192 qwen3-coder<\/code>, and tags such as <code>router:label:CODE<\/code> and <code>router:via:rule<\/code>, merged with anything the caller sent.<\/p>\n\n\n\n<div class=\"wp-block-group has-pale-cyan-blue-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:32px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Thinking about private AI for your organisation?<\/strong> We design, build and secure on-premises AI platforms for Australian organisations, with the guardrails this series describes built in from the start.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/local-ai\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-05-mid\" target=\"_blank\" rel=\"noreferrer noopener\">Explore Private AI<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"challenges-we-hit\">Challenges we hit<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">A tiny model that called plain questions code<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">With the original prompt, <code>qwen3:0.6b<\/code> labelled plain questions as CODE 9 times out of 20 on a realistic sample (&quot;What is the capital of France?&quot;, &quot;Explain how DNS works&quot;, &quot;How do tides work?&quot;) and 10 out of 20 on a held-out set of another 20. The prompt listed CODE first and gave definitions without examples. We listed QA first, added &quot;if unsure, QA&quot;, and sent ten example question-and-answer pairs before the real message, because a model this small follows examples far better than definitions. It then scored 20 out of 20 on the sample and 16 out of 20 on the held-out set. The four misses are either caught by the rules anyway or are QA-versus-AGENTIC confusions that both go to gemma4. Near-misses such as <code>SIMPLEX<\/code> or <code>Label: QA<\/code> are now read correctly, and anything unreadable defaults to QA. In practice the rules now settle all 40 prompts in both test sets, so the classifier only sees genuinely ambiguous messages; its accuracy matters for those.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Everyday words that look like code<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Some signals are weak alone: a language name (&quot;python&quot;, &quot;sql&quot;), a code noun (&quot;function&quot;, &quot;regex&quot;, &quot;bug&quot;) or a code verb (&quot;refactor&quot;, &quot;fix&quot;). Everyday text is full of them. &quot;I have a query about tide times&quot;, &quot;fix a flat tyre&quot; and &quot;Express your answer as a fraction&quot; must never become CODE, so weak signals count only in pairs. A single one, as in &quot;Is Python a snake?&quot;, goes to the classifier.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A field name that took routing down<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The first deployment named the tier field <code>tier<\/code>. LiteLLM already uses <code>model_info.tier<\/code> for its own values, so it rejected every model entry and <code>auto<\/code> was down for about five minutes until we renamed it with a prefix. When you add your own metadata to another product&#8217;s config, prefix the field names.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Stale windows and a third warm model<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A few days later, while writing the memory budget into policy after a hard freeze (Part 3), we found two routing bugs. qwen3-coder and gemma4 still advertised a 65,536-token window after being capped at 32,768, so the router was sizing prompts against capacity that no longer existed. And <code>qwen3:14b<\/code> and <code>gemma3:12b<\/code> were still tagged warm. A second large model loaded beside qwen3-coder is exactly what we found during the freeze, and a third would have evicted the warm pair. Both moved to <code>cold<\/code>, with a specialist model, so <code>auto<\/code> can now choose only qwen3-coder or gemma4. After a reboot gemma4 took about 70 seconds to load cold and qwen3-coder 10.5 seconds, and before one hard freeze qwen3-coder had been loaded, unloaded and reloaded within five minutes. We can&#8217;t say that churn caused the freeze, but a router should never add to it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A routing note that agents copied<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each <code>auto<\/code> reply began with a short note, such as <code>`[router \u2192 gemma4: rule: question; QA \u2192 gemma4 (resident)]`<\/code>. Testers in Open WebUI liked knowing which model answered. To an agent, though, the note is just text. It gets saved in the history, uses context on every later turn and gets repeated, and the test Telegram agent started its replies with <code>[router \u2192 \u2026]<\/code>. Requests that carry <code>tools<\/code> now skip the note unless a setting turns it back on.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-result\">The result<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Routing an <code>auto<\/code> request went from 9 seconds to 0.4 to 0.5 seconds. In the same evening&#8217;s measurements both big models stayed resident, the classifier loaded on the main instance zero times, and a direct qwen3-coder call answered in 2.5 seconds. Most of the gain came from not swapping models, not from a smarter decision.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The trade-offs are deliberate. <code>auto<\/code> chooses between two models, not five, because more choice would mean more swapping. Rules need upkeep when new everyday text looks like code, and 113 offline unit tests, including both test sets, make that safe. Each resident model has one slot, so a second caller to the same model still waits in Ollama&#8217;s queue. Routing decides which queue a request joins; it doesn&#8217;t shorten it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"do-it-yourself-rules-first-routing-with-a-small-resident-set\">Do it yourself: rules-first routing with a small resident set<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">What you need: a GPU server running Ollama in Docker behind a LiteLLM proxy, two models that fit in memory together, and a small Python web service such as FastAPI. Allow a day.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li>\n<p><strong>Keep two models warm.<\/strong> Set <code>OLLAMA_MAX_LOADED_MODELS=2<\/code> and a keep-alive of at least a few minutes on the main instance. Check: after one request to each, <code>curl -s localhost:11434\/api\/ps<\/code> lists both.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Give the classifier its own instance<\/strong>, with its own model volume, on another loopback port.<\/p>\n<pre><code class=\"language-yaml\">  ollama-helpers:\n    image: ollama\/ollama@sha256:&lt;digest&gt;\n    restart: unless-stopped\n    ports:\n      - &quot;127.0.0.1:11436:11434&quot;\n    volumes:\n      - ollama-helpers:\/root\/.ollama\n    environment:\n      OLLAMA_KEEP_ALIVE: &quot;24h&quot;\n      OLLAMA_MAX_LOADED_MODELS: &quot;2&quot;\n<\/code><\/pre>\n<p>Pull <code>qwen3:0.6b<\/code> into it. Hard-linking weights from the main instance saves disk only if both volumes sit on the same filesystem. Check: the main instance&#8217;s <code>\/api\/ps<\/code> never lists the classifier.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Tag models in the gateway.<\/strong> Add prefixed custom fields to each entry&#8217;s <code>model_info<\/code>, as in the example above, and mark only your two resident models as warm and routable. Check: <code>GET \/model\/info<\/code> shows the fields, and the proxy starts without rejecting any entry.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Write the router.<\/strong> The core is one function: the cheapest decision first, then filter, then prefer what is loaded.<\/p>\n<pre><code class=\"language-python\">def route(req):\n    label = slash_command(req) or rules(req) or classify(req)  # classifier last\n    need = CAPABILITY[label]                     # &quot;code&quot;, &quot;tools&quot; or &quot;chat&quot;\n    size = rough_estimate(req) + req.get(&quot;max_tokens&quot;, DEFAULT_CAP[label])\n    loaded = loaded_models()                     # Ollama \/api\/ps, cached 5 s\n    candidates = [m for m in gateway_models()    # gateway \/model\/info\n                  if m.routable and m.site_tier == &quot;warm&quot;\n                  and need in m.capabilities and m.context_window &gt;= size]\n    if not candidates:\n        return error(413, &quot;context_too_large&quot;, suggest=smallest_fit(size))\n    candidates.sort(key=lambda m: (m.name not in loaded, PREFERENCE.index(m.name)))\n    return forward(req, candidates[0])\n<\/code><\/pre>\n<\/li>\n\n\n<li>\n<p><strong>Write the rules first.<\/strong> Tools present means agent, code markers mean code, very short text means small talk, and a question means QA. Count weak signals only in pairs. Check: unit tests with everyday sentences like &quot;fix a flat tyre&quot; all come out as non-code.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Add the classifier last.<\/strong> Send only undecided messages, with ten labelled examples ahead of them, and default to QA when the reply can&#8217;t be parsed. Check: on a set of 20 plain questions, none comes back as CODE, and a prompt bigger than every window gets a 413, never a truncated answer.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Expose it as <code>auto<\/code><\/strong> in the gateway&#8217;s <code>model_list<\/code>. The <code>openai\/<\/code> provider insists on an API key, so pass one even if the router ignores it (load a real one with <code>os.environ\/<\/code> if the router checks keys), and include <code>\/v1<\/code> in the base URL.<\/p>\n<pre><code class=\"language-yaml\">  - model_name: auto\n    litellm_params:\n      model: openai\/auto\n      api_base: http:\/\/127.0.0.1:&lt;router-port&gt;\/v1\n      api_key: none   # any non-empty value; the router ignores it\n    model_info:\n      max_input_tokens: 26000\n<\/code><\/pre>\n<p>Check: a request for <code>auto<\/code> returns an answer, and the trace shows which model served it.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Test for thrash.<\/strong> Run two clients that alternate 20 code prompts and 20 chat prompts. Check: after warm-up, <code>\/api\/ps<\/code> still lists the same two models and Ollama&#8217;s log shows no new model loads.<\/p>\n<\/li>\n\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"key-takeaways\">Key takeaways<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>On one GPU, the expensive part of routing is a model swap. Measure end-to-end latency before tuning the classifier.<\/li>\n\n\n<li>Use rules first and a small model last, and give that model examples rather than definitions.<\/li>\n\n\n<li>Keep one list of models in the gateway. A second list will drift.<\/li>\n\n\n<li>Choose by capability first and let residency break ties.<\/li>\n\n\n<li>Never fall back to the cloud silently, and refuse clearly instead of truncating.<\/li>\n\n\n<li>Separate what people see from what agents receive, because a helpful note to a person is just extra tokens to an agent.<\/li>\n\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-light-green-cyan-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:40px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h2 class=\"wp-block-heading\">Planning private AI?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Catharsis Cyber Security is a Sydney cyber security company. We design, build and secure private AI platforms like the one in this lab, and help you decide where local or cloud AI fits.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/contact-us\/?type=contact&amp;service=local-ai&amp;utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-05-contact\" target=\"_blank\" rel=\"noreferrer noopener\">Talk to us about AI<\/a><\/div>\n\n\n<div class=\"wp-block-button is-style-outline is-style-outline--1\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/secure-ai\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-05-end\" target=\"_blank\" rel=\"noreferrer noopener\">Explore Secure AI<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Keep reading the series<\/strong><\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button is-style-outline is-style-outline--2\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">\u2190 Part 4<\/a><\/div>\n\n\n<div class=\"wp-block-button is-style-outline is-style-outline--3\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/running-local-ai-in-production-series-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Series guide<\/a><\/div>\n\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/gitops-for-a-gpu-ai-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">Next: Part 6 \u2192<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Our first smart router made every request slower and pushed other clients&#8217; models out of memory. This is how we rebuilt it in the lab, with rules before models, resident models first and clear refusals, and how to build one yourself.<\/p>\n","protected":false},"author":2,"featured_media":1096,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[69],"tags":[83,101,99,85,81,75,98,80,100,79],"class_list":["post-1097","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","tag-ai-lab","tag-gemma","tag-gpu-memory","tag-langfuse","tag-litellm","tag-local-llm","tag-model-routing","tag-ollama","tag-qwen","tag-unified-memory"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11) - Catharsis | Blog<\/title>\n<meta name=\"description\" content=\"How do you run several AI models on one GPU without slowdowns? Keep two resident, route with rules first, and never make Ollama swap models.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11) - Catharsis | Blog\" \/>\n<meta property=\"og:description\" content=\"How do you run several AI models on one GPU without slowdowns? Keep two resident, route with rules first, and never make Ollama swap models.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/\" \/>\n<meta property=\"og:site_name\" content=\"Catharsis | Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-13T23:30:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-11-03T09:27:58+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-05.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1280\" \/>\n\t<meta property=\"og:image:height\" content=\"720\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Sajal Jain\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Sajal Jain\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"18 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/\"},\"author\":{\"name\":\"Sajal Jain\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\"},\"headline\":\"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11)\",\"datePublished\":\"2026-09-13T23:30:00+00:00\",\"dateModified\":\"2026-11-03T09:27:58+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/\"},\"wordCount\":3371,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-05.jpg\",\"keywords\":[\"AI lab\",\"Gemma\",\"GPU memory\",\"Langfuse\",\"LiteLLM\",\"local LLM\",\"model routing\",\"Ollama\",\"Qwen\",\"unified memory\"],\"articleSection\":[\"Artificial Intelligence\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/\",\"name\":\"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11) - Catharsis | Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-05.jpg\",\"datePublished\":\"2026-09-13T23:30:00+00:00\",\"dateModified\":\"2026-11-03T09:27:58+00:00\",\"description\":\"How do you run several AI models on one GPU without slowdowns? Keep two resident, route with rules first, and never make Ollama swap models.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#primaryimage\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-05.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-05.jpg\",\"width\":1280,\"height\":720,\"caption\":\"A small network switch with patch cables\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/smart-routing-single-gpu-local-llm\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"name\":\"Catharsis | Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\",\"name\":\"Catharsis | Blog\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"width\":215,\"height\":60,\"caption\":\"Catharsis | Blog\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\",\"name\":\"Sajal Jain\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"caption\":\"Sajal Jain\"},\"description\":\"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/author\\\/sajal-jain\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11) - Catharsis | Blog","description":"How do you run several AI models on one GPU without slowdowns? Keep two resident, route with rules first, and never make Ollama swap models.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/","og_locale":"en_US","og_type":"article","og_title":"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11) - Catharsis | Blog","og_description":"How do you run several AI models on one GPU without slowdowns? Keep two resident, route with rules first, and never make Ollama swap models.","og_url":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/","og_site_name":"Catharsis | Blog","article_published_time":"2026-09-13T23:30:00+00:00","article_modified_time":"2026-11-03T09:27:58+00:00","og_image":[{"width":1280,"height":720,"url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-05.jpg","type":"image\/jpeg"}],"author":"Sajal Jain","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Sajal Jain","Est. reading time":"18 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#article","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/"},"author":{"name":"Sajal Jain","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099"},"headline":"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11)","datePublished":"2026-09-13T23:30:00+00:00","dateModified":"2026-11-03T09:27:58+00:00","mainEntityOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/"},"wordCount":3371,"commentCount":0,"publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-05.jpg","keywords":["AI lab","Gemma","GPU memory","Langfuse","LiteLLM","local LLM","model routing","Ollama","Qwen","unified memory"],"articleSection":["Artificial Intelligence"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/","url":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/","name":"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11) - Catharsis | Blog","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#primaryimage"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-05.jpg","datePublished":"2026-09-13T23:30:00+00:00","dateModified":"2026-11-03T09:27:58+00:00","description":"How do you run several AI models on one GPU without slowdowns? Keep two resident, route with rules first, and never make Ollama swap models.","breadcrumb":{"@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#primaryimage","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-05.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-05.jpg","width":1280,"height":720,"caption":"A small network switch with patch cables"},{"@type":"BreadcrumbList","@id":"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/catharsis.net.au\/blog\/"},{"@type":"ListItem","position":2,"name":"How Do You Run Several AI Models on One GPU Without Slowdowns? (Part 5 of 11)"}]},{"@type":"WebSite","@id":"https:\/\/catharsis.net.au\/blog\/#website","url":"https:\/\/catharsis.net.au\/blog\/","name":"Catharsis | Blog","description":"","publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/catharsis.net.au\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/catharsis.net.au\/blog\/#organization","name":"Catharsis | Blog","url":"https:\/\/catharsis.net.au\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","width":215,"height":60,"caption":"Catharsis | Blog"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099","name":"Sajal Jain","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","caption":"Sajal Jain"},"description":"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management","url":"https:\/\/catharsis.net.au\/blog\/author\/sajal-jain\/"}]}},"_links":{"self":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1097","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/comments?post=1097"}],"version-history":[{"count":4,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1097\/revisions"}],"predecessor-version":[{"id":1218,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1097\/revisions\/1218"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media\/1096"}],"wp:attachment":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media?parent=1097"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/categories?post=1097"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/tags?post=1097"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}