Contents
Articles inference local_llm_selection

Choosing local LLMs for a two-model pipeline

Two models enter, one inference leaves

Model selection, quantization, and LM Studio configuration for a two-model inference pipeline on 32 GB of VRAM.

Also, why is my small model so slow?

Project Continuum Engine Authors @Tristan & @Claude Topic Inference
Created 2026-03-15 Updated 2026-09-28 Version 1.3

Preface

▶

Article Preface: This isn't a "do more with a Raspberry Pi" article.

Everything described here runs on dedicated, and quite frankly expensive, hardware. If someone tells you multi-model local inference is accessible on commodity hardware, they're either running tiny models or lying about the latency.

Hardware specs, KV cache, anti-capitalistic rhetoric . . .

Primary - Desktop Workstation

GPU NVIDIA RTX 5090 - 32 GB GDDR7
CPU AMD Ryzen 9 9950X3D
RAM 96 GB DDR5 @6400 MHz
RT CUDA 13.2 · Latest stable drivers

Secondary - Laptop (via LM Link)

GPU NVIDIA RTX 4090 Mobile
CPU Intel Core i9-14900HX
RAM 32 GB DDR5 @5200 MHz

The 32 GB of VRAM on the primary GPU is the constraint that shapes every decision in this article. A 22 GB model doesn't leave you 10 GB of headroom. The KV cache lives in VRAM too, and it can be enormous. Think of it like a notebook the model keeps while reading - every token it's ever seen in the conversation gets a page. The model weights are the brain; the KV cache is the notebook. A 262K-token context window means 262K pages pre-allocated whether you've written on 50 of them or not. Even at a sensible 4K context, you're looking at hundreds of megabytes per loaded model. Two models with their KV caches, plus inference engine overhead - 32 GB fills up faster than the model file sizes suggest.

The 96 GB of system RAM matters for MoE models that can offload expert weights to CPU - but if you're offloading to system RAM, you've already lost the latency game for a router that needs sub-second responses. Everything we care about fits in VRAM or it doesn't ship.

The industry is pushing machine learning harder than it did non-fungible tokens, and at the same time it has total control of the hardware, with artificial scarcity and monopolistic pricing. This is not how you start a movement for the betterment of humankind. The barriers are intentional.

Remember, comparison is the thief of joy - so scream into a pillow for around five minutes. Just long enough to repress it all once more.

The Pipeline

@Tristan

I'm building an inference pipeline that needs to curate context for Cortex, classify incoming prompts, extract structured data, and route requests to the right model. I've got 32 GB of VRAM. What local model should I use for the fast stuff - the classification, the structured extraction, the routing decisions - without burning all my VRAM on a model that's overkill for the task?

One model doesn't seem to do two different and distinct jobs very well.

My instinct was to load the biggest model that fits in VRAM and send everything to it. Get a prototype working first, optimize later. This failed immediately.

When I needed the system to classify a prompt in under 300ms while also generating a nuanced multi-paragraph response, it kind of sucked. A 30B model can do both, but it takes 2-4 seconds to tell you something is a simple factual lookup. By then, you've already paid the latency for a task a 4B model handles in 100ms. Or should handle in 100ms - more on that below.

The pattern I thought might solve it: triage, then generate. A small, fast model classifies the incoming prompt - determines intent, extracts entities, estimates complexity, decides routing. A large model does the actual generative work. The small model adds 100-300ms to the pipeline. The large model's generation time dominates regardless.

The win I hadn't considered was the short-circuit. If the small model can handle the request alone - simple classification, entity extraction, structured data transformation - the large model never has to load. The best kind of optimization, an entire call eliminated.

I still had a concern, though. Won't the smaller model have accuracy issues? Garbage in, garbage out. Anyone who's used Copilot or Cursor on "Auto" model selection knows it can be rough. Personally, that's a setting I change immediately at work.

Model Selection

@Tristan

Okay, I'm sold on the two-model approach. But I've got a dozen models downloaded in LM Studio already - dense transformers, MoE models, different sizes. Which ones specifically? What should I actually load as the router and what as the workhorse?

Two models, two distinct jobs, 32 GB of VRAM.

After testing every model that would fit on a 32 GB card, the selection came down to architecture more than parameter count. Claude had me think of it like staffing. A dense transformer is an office where every employee works on every task - all 32 billion of them show up for a one-line classification. A mixture-of-experts (MoE) model is an office with 128 specialists where only 6 get paged for any given task. Same total headcount, fraction of the cost per request. I was sold on the analogy but really doubted the performance impact it would have.

The router: Nemotron-3-Nano-4B

2.8 GB at Q4_K_S. Hybrid Mamba-Transformer architecture - 42 layers total: 21 Mamba-2, 4 Attention, 17 MLP [1]. Compressed from Nemotron-Nano-9B-v2 using NVIDIA's Elastic framework - router-guided structured pruning across Mamba heads, hidden dimensions, FFN channels, and depth, followed by two-stage distillation on 63B+ tokens [1]. Not trained from scratch at 4B.

State-of-the-art instruction following for its size class on IFBench and IFEval. Strong tool-use performance. Competitive hallucination avoidance. NVIDIA's own GGUF release is Q4_K_M at 2.84 GB, benchmarked at 18 tokens/second on a Jetson Orin Nano 8GB [2]. On a desktop GPU with headroom, it's considerably faster.

This model exists specifically for the job we're giving it. NVIDIA built it for edge deployment and agentic routing - their published use cases include local voice assistants and AI game NPCs [2]. We're using it for the routing half: classifying prompts, extracting structured fields, and checking the 30B's output.

The workhorse: Nemotron-3-Nano-30B-A3B

31.6B total parameters, but only 3.2B active per token - 128 experts, 6 consulted per request, the rest stay quiet. At Q4_K_S it's ~22 GB. That sounds like 10 GB of headroom on a 32 GB card - until you account for the KV caches. Both models' attention state lives in VRAM alongside the weights.

The MoE architecture is why this works. A dense 32B model like QwQ-32B activates all 32 billion parameters on every token. This model activates 3.2 billion. The generation speed is closer to a small model than a large one, but the reasoning quality comes from having 30B+ parameters available for specialization. Trained on 25 trillion tokens with multi-environment reinforcement learning via synchronous GRPO.

NVIDIA reports up to 3.3x higher throughput than Qwen3-30B-A3B and 2.2x higher than GPT-OSS-20B. It outperforms both on standard benchmarks. Supports 1M token context if you need it - though you shouldn't, for reasons covered below.

The alternative: Qwen3-Coder-30B-A3B

Same MoE trick - 30B total, ~3B active - but at 17.5 GB (Q4_K_S), leaving more VRAM headroom. Strong on code generation and structured output. When the pipeline is code-heavy, this is the better pick for the workhorse slot.

What we rejected and why

QwQ-32B (19.9 GB, dense) and Gemma-3-27B (16.4 GB, dense) both produce good output. Both are noticeably slower at generation because every parameter is active on every token. For interactive inference where the user is watching tokens stream in, that speed difference is the difference between "responsive" and "waiting." MoE models with comparable quality and 3x the throughput make dense models hard to justify for this use case.

Configuration

@Tristan

I loaded Nemotron-4B as the router. It's crawling. Multiple seconds for a one-word classification. On an RTX 5090. I do need to mention I left the context size at the model's max on purpose - doesn't a larger context window help reduce hallucinations? And I set max tokens high to roughly 10K, so it doesn't cut off mid-response, an issue I did have when using one model. What am I doing wrong?

The settings matter.

Apparently, this is a common misconception. Context length and hallucination prevention are unrelated settings solving unrelated problems. Context length is how much memory the inference engine allocates for the KV cache - the data structure that tracks attention across tokens. A 262K context window allocates memory for 262K tokens of attention state whether your prompt is 50 tokens or 200K. Max tokens is how much output buffer is reserved; larger canvases really allow models free rein.

Neither setting makes the model explicitly more accurate. A 4B model that is classifying prompts into four categories doesn't hallucinate less with a 262K context window - it hallucinates less with grammar constraints, structured output schemas, and temperature 0.0. What the oversized context window does do is burn VRAM on a KV cache that can be several times larger than the model weights themselves, and slow every forward pass to a crawl, even for trivial prompts.

The model wasn't slow and the settings were wrong.

Context length is the #1 performance killer

LM Studio defaults context length to the model's advertised maximum. For Nemotron-4B, that's 262K tokens. For a router model classifying prompts, you need 2,048-4,096. The KV cache for a 262K context window can be several times larger than the model weights. Every forward pass works through that memory structure even if your actual prompt is 50 tokens.

Flash Attention plus GPU KV cache offload delivers up to 45x speedup in time-to-first-token at 4K context.

Max tokens: budget what you'll actually use

Setting max output tokens to thousands for a classifier that returns one word is wasteful - the inference engine allocates memory for the full generation budget upfront. For classification, 128-256 tokens. For structured extraction, 512-1024. Match the budget to the task.

The full checklist for a fast router

GPU offloadmax (all layers on GPU)
Context length2048-4096
Max tokens128-256
Flash Attentionon
KV Cache on GPUon
Reasoning modeoff
Temperature0.0
Batch size (n_batch)512+

Reasoning mode deserves a specific callout. Nemotron models have a toggle - the thinking trace burns tokens without improving accuracy on constrained tasks. A classifier picking from an enum of four values doesn't need to "think."

One known quirk: setting n_gpu_layers to exactly the model's layer count can be slower than setting it one above. Set it to 49 instead of 48. Nobody knows why. It works.

Reliability

@Tristan

Okay, so context size doesn't prevent hallucinations. Then what does? If I'm trusting a 4B model to be the gatekeeper for the entire pipeline - classifying prompts, deciding routing, extracting structured data - how do I stop it from just making things up? It's a 4B model. It's not exactly GPT-4.

Never let the fast model freestyle.

The 4B model is the gatekeeper. If it hallucinates, garbage flows downstream and the 30B model generates confidently wrong output from confidently wrong input. The entire pipeline's reliability ceiling is the router's accuracy floor.

Constrain the output space

For classification, give closed-form tasks with explicit valid outputs. "Classify into exactly one: CODE, REASONING, SIMPLE, CONTEXT_LOOKUP. Respond with only the category name." The model can't hallucinate a fifth category if the grammar constraint won't let it.

GBNF grammar constraints in LM Studio enforce this at the token level - regex-strength enforcement over the output space. The model physically cannot produce invalid output.

Structured output for extraction

JSON schema with grammar constraints eliminates hallucination in extraction tasks. The model produces a fixed structure and nothing else. It cannot invent fields or omit required fields.

{
                    "intent": "...",
                    "key_entities": [...],
                    "complexity": "low|medium|high"
                    }

Verification loops for agent-generated state

When the 4B decides an inference result should become a persistent context entry - a piece of knowledge the system remembers - validate it. Does the generated title relate to the source content? Is the classification consistent with the extracted entities? Agent-generated entries start at moderate confidence (0.50). The system's retrieval scoring naturally deprioritizes entries that don't corroborate with anything else.

Temperature 0.0 for all classification and routing calls. Max tokens inside the budgets above: 128-256 for classification, 512-1024 for structured extraction. Reasoning mode off. Each model stays in its lane. The 4B classifies, extracts, and routes. The 30B generates. The 4B validates the 30B's output.

Quantization

@Tristan

Once the configuration was right and the models were actually fast, I started wondering - can I squeeze more out of this? I went down the benchmark rabbit hole, comparing quantization methods, reading what other people were doing with the same models. That's where Unsloth kept coming up. Their quants are apparently faster and more accurate at the same bit depth. What's the deal - is it actually better than the standard GGUF, or is this just hype?

Unsloth Dynamic vs. standard GGUF - and when it doesn't matter.

Quantization is compression for model weights - like JPEG for neural networks. You're trading precision for size. A full-precision model stores every weight as a 16-bit number; a 4-bit quant rounds those down to save 75% of the space. For a model picking from an enum of four values, the rounding barely matters. But for generative tasks, the differences are real - and how you round matters more than how much.

NVIDIA's approach: selective precision by architecture knowledge

NVIDIA's FP8 quantization of the 4B model is hand-tuned with architectural awareness. They keep all 4 self-attention layers in BF16, plus the 4 Mamba layers immediately preceding them. All Mamba Conv1D operations stay in BF16. Everything else gets FP8 [1]. The result: 100% median accuracy recovery across target benchmarks compared to the BF16 model, with up to 1.8x improvement in latency and throughput on DGX Spark and Jetson Thor [1].

Their GGUF release takes a different path. The Q4_K_M quantization via llama.cpp also achieves 100% median accuracy recovery - and on some benchmarks actually outperforms BF16. IFBench scores jump from 43.2 (BF16) to 46.9 (Q4_K_M) on prompt-level and from 44.2 to 49.6 on instruction-level [2]. Quantization noise occasionally helps generalization. Sometimes you get lucky.

Unsloth's approach: per-layer profiling by KL Divergence

Unsloth Dynamic 2.0 doesn't rely on architectural knowledge of which layers are sensitive. Every possible layer gets profiled against KL Divergence - the distance between the original model's token probability distribution and the quantized version's [4]. The combinations differ per layer, per model. Layers quantized in Gemma 3 differ from those in Llama 4, which differ from those in Nemotron [3].

The calibration data matters as much as the method. Standard imatrix GGUFs calibrate on Wikipedia-adjacent text, which optimizes for perplexity on similar text. Unsloth calibrates on 300K to 1.5M tokens of hand-curated chat, tool-calling, and coding data [4]. Instruct models have unique chat templates - using text-only calibration data for an instruct model is testing the wrong thing.

The optimization target is different too. Most quantization benchmarks report perplexity, where output token errors can cancel out. Unsloth optimizes for KL Divergence, which correlates with answer "flips" - answers changing from correct to incorrect or vice versa [4]. MMLU might not decrease with quantization, but that's because some incorrect answers flip to correct while correct ones flip to incorrect.

Where it matters for hybrid Mamba-Transformer models

Hybrid architectures like Nemotron-4B have a specific problem. NVIDIA's own FP8 research shows that Mamba output projection layers lose up to 40% of their values to zero when quantized aggressively - the small numbers get rounded to nothing, and 40% of the layer's signal just vanishes. That's why they keep those layers at higher precision in their official release [1]. Unsloth's per-layer profiling on similar hybrid architectures confirms this pattern: ssm_out layers dramatically increase KL Divergence when quantized, and the disk space savings are negligible [4]. Quantizing any attn_* layers is especially sensitive for hybrid architectures [4].

Standard GGUF quantizers don't know about any of this. They apply the same bit width to all layers of a given type - like compressing every instrument in a song at the same bitrate, whether it's a vocal track or background noise. An ssm_out layer gets the same treatment as an ffn_up layer, even though one is catastrophically sensitive and the other tolerates 3-bit quantization with minimal quality loss [4].

The pragmatic answer for a router model

For classification tasks, both approaches work. Picking from an enum of 4-6 options depends on high-level instruction following, which survives even aggressive quantization - NVIDIA's Q4_K_M actually scores higher than BF16 on IFBench [2]. For generative tasks (prompt rewriting, summarization, structured extraction), Unsloth's Dynamic quants have an edge from selective layer preservation and chat-oriented calibration.

But with 32 GB of VRAM and a 2.8 GB model, the pragmatic answer is: run Q8_0 (~4 GB) for near-lossless quality. You're not memory-constrained on the router. The quantization debate matters for the 30B workhorse that barely fits - not for the 4B model with 20 GB of headroom.

Pipeline

The 4B router stays loaded in front of one 30B workhorse.

Nemotron-4B stays loaded as the router. Tiny VRAM footprint, low context window, instant responses. One 30B MoE model loads as the workhorse - Nemotron-30B for general reasoning, Qwen3-Coder-30B when the workload is code-heavy. The 4B classifies the prompt, decides whether to handle it alone or prep and forward to the 30B. Total pipeline latency is dominated by the large model's generation time. The router adds ~100-300ms.

Two instances, two quantizations - on purpose

The router runs at Q8_0. The workhorse runs at Q4_K_S. These aren't the same model at different sizes - they're different models with different jobs - but even if they were, you'd still want different quantization per role.

The router's job is fragile - like a switchboard operator. It doesn't need to be eloquent, but it absolutely cannot connect you to the wrong line. Picking from an enum of four categories, extracting structured JSON, deciding whether to short-circuit - these tasks live on the margin between correct and wrong. A single flipped token in a classification response sends garbage downstream. Higher precision (Q8_0, ~4 GB for a 4B model) costs almost nothing in VRAM and eliminates quantization-induced errors on the component where errors are most expensive.

The workhorse's job is tolerant. Generating a multi-paragraph response with 30B+ parameters backing it has natural redundancy - the model can route around minor quantization artifacts through sheer capacity. Running it at Q4_K_S (~22 GB) leaves room for the router's Q8_0 (~4 GB) plus both models' KV caches within 32 GB. That allocation spends precision where the task is brittle and compression where the task is robust.

The alternative - switching quantization per request - doesn't exist. GGUF weights are loaded at server startup. Changing quantization means unloading the model, loading a different weight file, and reinitializing. On a 22 GB model, that's seconds of downtime in a pipeline designed to add 100-300ms. Each instance is tuned - context length, max tokens, quantization, temperature - for exactly one job.

LM Studio and LM Link

LM Studio supports multiple loaded models simultaneously - you can have the 4B router and the 30B workhorse loaded in the same instance and route API calls to each by model name. That's enough for a single-machine pipeline.

LM Link takes it further. It connects multiple machines running LM Studio into a mesh VPN - models loaded on remote devices appear as if they're local, accessible through the same localhost:1234 endpoint. Any tool that already connects to your local LM Studio server works with remote models transparently. In our case, the desktop's RTX 5090 runs the 30B workhorse while a secondary machine with an RTX 4090 Mobile handles the router - dedicated silicon for each role, no VRAM contention, end-to-end encrypted over Tailscale.

For a pipeline that's meant to be invisible, this is the right architecture. The router runs on its own GPU with its own tuned context window and quantization. The workhorse gets the full 32 GB of the 5090. Neither model competes for VRAM, and the API surface is identical to a single-machine setup.

What's Next

The router becomes the assembler.

Right now the router decides which model to call. In Continuum Engine's Milestone 1, it decides what context to assemble before calling it. The three-tier context enrichment system - PostgreSQL for structured truth, Qdrant for semantic vector search, FalkorDB for graph relationships - gives the router a much larger decision space. Classify the prompt, query the relevant stores, score and assemble the context, then hand the enriched prompt to the 30B model.

Each component improves independently without the others needing to know about it.

← Back to Articles