<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Herald — Building Private AI]]></title><description><![CDATA[Building a fully local, private AI assistant over 20 years of personal documents. No cloud APIs. Real engineering decisions, real failures, real fixes.]]></description><link>https://herald-ai.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 15 Sep 2026 17:57:02 GMT</lastBuildDate><atom:link href="https://herald-ai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[In, Store, Retrieve: The Three Places RAG Quietly Fails — And How We Fixed Each One]]></title><description><![CDATA[When I last wrote about this project, I was benchmarking enterprise AI inference tooling against a local alternative on cutting-edge GPU hardware — and discovering that enterprise frameworks are not a]]></description><link>https://herald-ai.hashnode.dev/in-store-retrieve-the-three-places-rag-quietly-fails-and-how-we-fixed-each-one</link><guid isPermaLink="true">https://herald-ai.hashnode.dev/in-store-retrieve-the-three-places-rag-quietly-fails-and-how-we-fixed-each-one</guid><category><![CDATA[AI infrastructure]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[llm]]></category><category><![CDATA[document ai]]></category><category><![CDATA[Docker]]></category><category><![CDATA[vector database]]></category><category><![CDATA[vector embeddings]]></category><category><![CDATA[VectorSearch]]></category><category><![CDATA[vectors]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[MachineLearning]]></category><category><![CDATA[bm25]]></category><category><![CDATA[Hybrid RAG Systems]]></category><category><![CDATA[Retrieval-Augmented Generation]]></category><dc:creator><![CDATA[Ravi Mohta]]></dc:creator><pubDate>Tue, 17 Mar 2026 12:21:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/aeedc335-7ae3-4b90-8fa3-7113d48c3477.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I last wrote about this project, I was benchmarking enterprise AI inference tooling against a local alternative on cutting-edge GPU hardware — and discovering that enterprise frameworks are not always ready for the newest consumer hardware. That was a story about infrastructure friction.</p>
<p>The work since then has been about something harder: making a system that works well enough that you actually trust it with real questions about real documents.</p>
<p>Ten build sessions. 17 Docker containers. A vector store holding over 5,600 documents spanning more than a decade of financial and personal paperwork. Here is an honest account of the problems encountered, the false starts, and what ended up working.</p>
<h2>The Problem With "Working"</h2>
<p>After the initial RAG pipeline was running, it looked impressive. You could ask a question and get an answer. But live testing against real documents revealed a pattern anyone who has built a production RAG system will recognise: it worked when you already knew what to expect, and failed precisely when you needed it most.</p>
<p>Three specific failure modes drove most of the work that followed.</p>
<p><strong>Documents that existed in the system were not being retrieved.</strong> The retrieval logic was too narrow. Keyword extraction only identified formal proper nouns, missing common domain terms that appeared in conversational queries. Search was capped at a handful of candidates per term, and both the keyword and semantic tracks used a flat merge that scored every keyword hit equally regardless of specificity or frequency.</p>
<p><strong>The system was confusing document dates with the current date.</strong> The system prompt included today's date for context. When a user asked "what date is the offer valid until", the model blended today's date with the date field it found in the document — and reported the wrong answer with confidence. There was no explicit instruction preventing date substitution.</p>
<p><strong>Rich structured metadata was completely invisible to retrieval.</strong> The AI was extracting valuable structured information from every document — document type, sender category, financial keywords, summaries — and storing it in a separate structured database. But none of that information was being placed into the vector index. Queries that depended on that structured data returned nothing, even when the information clearly existed.</p>
<p>Each failure exposed a fundamental design assumption that needed correcting. The most important of these — the invisible metadata problem — also had the largest downstream impact.</p>
<h2>The Invisible Data Problem</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/6aee1612-39ed-4b2a-b899-9e17da82e263.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 1: Before and after the metadata fix — AI-extracted structured fields are now appended to document content before chunking and embedding, making them findable at query time</em></p>
<p>The document processing pipeline fetched each document's text content, split it into overlapping chunks, generated vector embeddings for each chunk, and stored them in the vector database. This is standard RAG practice. The problem was what it was not doing.</p>
<p>The document management system stored AI-extracted metadata in eight custom fields per document — property type, sender category, document keywords, structured summary, and more. The processing pipeline fetched this data as part of its document retrieval call — and then silently discarded it, indexing only the raw OCR text.</p>
<p>The fix was architecturally simple but operationally significant: append all non-empty custom fields as a clearly labelled metadata block at the end of each document's content before chunking. Every vector chunk generated from that document then carries the metadata context. A query for a specific financial figure or document category reaches the metadata block directly.</p>
<p>The same field mapping must be maintained in two separate places — the webhook-triggered document processor and the bulk backfill script — because they follow different code paths. A new field added in one place but not the other creates a silent gap in index coverage.</p>
<p>The critical lesson: a document AI system is only as good as what you actually place in the index. Rich structured information sitting in a separate database is functionally invisible to any retrieval system that does not know to look there. This single fix resolved more user-facing query failures than any subsequent retrieval algorithm improvement.</p>
<h2>Backfill at Scale: When Parallelism Exposes Hidden Assumptions</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/96ebe88e-6118-4947-a74d-72d42dca1efc.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 2: Self-healing backfill architecture — parallel throughput phase catches most documents; the coverage check and sequential retry phase guarantees completeness</em></p>
<p>Fixing the invisible metadata problem required rebuilding the entire vector index — all 5,600 documents needed to be reprocessed through the corrected pipeline. A parallelised approach using 16 concurrent workers processed documents much faster than a sequential approach — and immediately exposed a capacity problem that sequential processing had been hiding.</p>
<p>Under sustained concurrent load, the document management backend began timing out on content fetch requests. The retry mechanism exhausted its attempts for 181 documents, which were logged as failures. A post-backfill coverage check revealed the scale of the gap: nearly 400 documents were absent from the vector index compared to the source system.</p>
<p>A second issue compounded the problem. The backfill orchestration script was supposed to increase the AI model server's parallelism setting before running. This was silently failing — the script was editing the base systemd service configuration file, but the parallelism setting actually lives in a drop-in override file in a different directory. The change was applied to the wrong file, reported success, and the service restarted unchanged.</p>
<p>The corrected orchestrator addresses both issues. The parallel phase handles throughput. After it completes, the orchestrator computes the difference between all document IDs in the source system and all document IDs present in the vector index, then sequentially retries every gap until coverage is complete. It also explicitly verifies that the model server configuration change took effect before starting.</p>
<p>The broader pattern: parallelism surfaces capacity assumptions that serial processing hides. A system that reliably processes one document per second may fail at sixteen simultaneously — and the failure mode is often silent data gaps rather than visible errors.</p>
<h2>Upgrading Retrieval: From Text Matching to Hybrid Search</h2>
<p>Retrieval did not get fixed in one step — it went through four distinct generations over the course of this project, each one addressing the failure modes the previous version left behind. The diagram below shows the end state: the complete query pipeline as it stands today.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/325307ff-4492-4b88-a4d0-05d6c4736674.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 3: The complete Herald query pipeline — LLM analysis, hybrid BM25 + dense search, cross-encoder reranking, and guardrailed generation</em></p>
<p>Before getting into how each component works, it is worth understanding the journey that led here. The system started with basic substring keyword matching and progressively replaced each weak link — first with real BM25 scoring, then with semantic embeddings, then with a cross-encoder reranker that scores query-document pairs directly. Each generation fixed a specific class of failure. The diagram below maps that evolution.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/44738e92-c337-47b0-8bf2-b9b562398ec6.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 4: Four generations of retrieval — what each approach got right, what it left broken, and how many chunks ultimately reached the LLM at each stage</em></p>
<p>The central insight that drove most of these changes: dense semantic embeddings and sparse BM25 vectors are not competing approaches — they catch fundamentally different types of queries. Neither is sufficient on its own. The diagram below shows exactly what each method handles well and where it fails, which is why the architecture uses both together.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/512d521d-77ef-4d13-b7c0-5d091fc17ce8.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 5: Dense vs sparse BM25 vectors — what each method catches and misses, and why hybrid search is necessary for real document retrieval</em></p>
<h3>The Problem With the Original Keyword Track</h3>
<p>The original keyword retrieval was not true keyword search — it was substring scanning with no understanding of term importance. Every occurrence of a common term across thousands of documents scored equally, whether it appeared once in a passing reference or fifty times in a directly relevant document. There was no concept of inverse document frequency — how rare a term is across the corpus — which is what makes keyword search precise.</p>
<p>The consequence: queries for common terms retrieved too much, while queries for specific reference numbers or financial figures retrieved too little.</p>
<h3>BM25 Sparse Vectors</h3>
<p>The replacement uses true BM25 scoring — the same algorithm that powers traditional search engines. Each document is represented as a sparse vector where each dimension corresponds to a unique token in the vocabulary, weighted by term frequency and inverse document frequency. Common words receive low weights; rare, distinctive terms receive high weights. This is generated at index time via a CPU-only library that runs in under a millisecond per query — fast enough to run on every request without impact.</p>
<p>The BM25 sparse vectors are stored alongside the dense semantic embeddings in the same vector database collection, and both searches run simultaneously as a native database operation. This is architecturally cleaner than running separate searches and merging results in application code.</p>
<h3>Dense Semantic Embeddings</h3>
<p>The dense embedding model encodes documents as high-dimensional floating-point vectors in a semantic space, where documents with similar meaning are positioned close together regardless of whether they share the same words. This means a search for "invoice" finds documents containing "bill" or "statement", and a natural language question about payment due dates finds payment schedule sections even without exact wording matches.</p>
<p>Dense search handles paraphrasing and conceptual similarity well, but fails on exact terms — a specific policy reference number has no semantic neighbours, so dense search will not find it reliably.</p>
<h3>Reciprocal Rank Fusion and Cross-Encoder Reranking</h3>
<p>The two search legs are combined via Reciprocal Rank Fusion — a method that merges rank positions from multiple result lists without requiring score normalisation across different scales. Documents appearing in both the sparse and dense results receive a significant scoring boost. The output is approximately 20 candidate chunks.</p>
<p>These candidates then pass to a cross-encoder reranker. Unlike the bi-encoder approach used for retrieval — where query and document are encoded separately and compared by cosine distance — a cross-encoder reads the query and each candidate document together in a single forward pass through a transformer model. This produces a much more accurate relevance score that directly reflects the relationship between what was asked and what the document contains.</p>
<p>The reranker was upgraded midway through the project from a 6-layer to a 12-layer model, improving precision meaningfully at acceptable CPU latency. An adaptive bypass was added: when multiple metadata filters are already applied to the search, the candidate pool is tight enough that reranking adds no value, and the step is skipped entirely — reducing response time from approximately 2 seconds to near-instant for filtered queries.</p>
<p>Only the top 5 reranked chunks reach the LLM. Fewer, better-selected chunks produce more accurate answers — the LLM focuses on directly relevant information rather than sifting through tangentially related content from other documents.</p>
<p>One final dimension of retrieval quality that is easy to overlook: how documents are chunked and what goes into each chunk matters just as much as which algorithm searches them. The diagram below captures this — including why the metadata embedding fix described earlier had such a disproportionate impact compared to algorithm changes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/84c9f8f9-4ddd-4a91-ba4c-5b67db97a17a.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 6: How chunking strategy and what goes into each chunk determines retrieval quality — the metadata embedding fix, and why dense embeddings alone are insufficient</em></p>
<h2>The Rules-Based Trap: Entity Resolution</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/f1fe6b14-634a-49f8-801b-971f1f2cd0ba.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 7: Rules-based entity resolution vs LLM-native query analysis — everything the rules-based approach required, and what replaced it entirely</em></p>
<p>A core capability the system needs is entity resolution: mapping what the user says to the structured entities that actually exist in the document management system. When a user asks about a specific document type or property, the system needs to understand which tag, correspondent, or document type in the catalogue they mean — and apply that as a precise filter on retrieval.</p>
<h3>Version 1: Rules-Based Matching</h3>
<p>The first implementation used a fuzzy matching engine with manually tuned confidence thresholds, stop word lists, minimum token length rules, acronym detection, a configuration file for keyword aliases, and a learned mappings file that persisted to disk. This system worked for the cases it was designed for, and failed for every edge case it had not seen. Short words triggered substring collisions with unintended entries. Phrases were learned as complete mappings rather than decomposed into components. Every new failure mode required a code change, a rebuild, and a redeployment.</p>
<p>The system accumulated approximately 200 lines of heuristics, two persistent files, and a configuration format — and still required constant maintenance as new query patterns emerged in production.</p>
<h3>Version 2: LLM-Native Analysis</h3>
<p>The replacement is a single call to the local LLM before any retrieval takes place. The model receives the user's raw query alongside the complete live catalogue — every tag name, document type, and correspondent name — fetched directly from the document management system at startup and refreshed on demand via an admin endpoint without requiring a container restart.</p>
<p>The model returns a structured analysis: a rewritten version of the query with context resolved, the exact matching values from the catalogue if applicable, any date range implied by the query, whether the query is exhaustive or looking for specific information, and a clarification question if the query is genuinely ambiguous.</p>
<p>A safety layer validates every returned entity value against the live catalogue before applying it as a filter. If the model returns a value that does not exist in the system, that filter is silently discarded and retrieval proceeds without it, rather than producing an empty result set. The LLM handles synonyms, abbreviations, partial names, and edge cases naturally. No code changes have been required for new query patterns since this version was deployed.</p>
<h2>The Exhaustive Query Problem</h2>
<p>Vector search is a ranking operation — it returns the most relevant K documents from a much larger set. This is the right behaviour for targeted questions. It is the wrong behaviour for exhaustive questions: "find everything matching these criteria."</p>
<p>When a user asks to find all documents for a particular property, or list every invoice from a specific supplier, a top-K retrieval system returns the highest-scoring results and stops. If there are 66 matching documents, 61 of them are invisible. This was discovered in testing when a query designed to retrieve all documents for a property returned 5 results, while a direct database count showed 66.</p>
<p>The solution routes exhaustive queries — detected by the LLM analysis step — to a completely separate code path. Instead of vector search, this path uses paginated scrolling through the vector database with payload filters applied. Every page of results is collected, document IDs are deduplicated, and the full set is returned with no ceiling. The property matching uses a five-tier filter cascade to handle OCR inconsistencies, partial addresses, and documents that reference a property without being explicitly tagged to it.</p>
<h2>Automatic Data Extraction: Hardening the Pipeline</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/94fc2763-d563-48df-bd8b-e54225583a46.png" alt="" style="display:block;margin:0 auto" />

<p><em>Figure 8: Webhook idempotency handling and flood protection — the state machine for managing multiple events per document and protecting against bulk operation overload</em></p>
<p>Running alongside the retrieval improvements was a separate service that automatically classifies incoming documents and extracts structured financial data from them. Every document that arrives triggers a two-stage AI pipeline: classify the document type, then extract the relevant structured fields. Extracted records enter a human review queue before being committed to the database — the AI does the extraction work, the human approves, corrects, or discards.</p>
<h3>The Webhook Storm Problem</h3>
<p>The document management system notifies downstream services of document changes via webhooks. The AI metadata enrichment process writes each extracted field one at a time — each write triggers a new webhook event. A single document being enriched generates between five and eight separate notification events in rapid succession.</p>
<p>A naive extraction handler processes each event independently, meaning each document triggers up to eight extraction attempts. The handler needed an explicit state machine: approved records are always skipped; discarded records are reset and re-extracted; pending records with high extraction confidence are treated as duplicates; pending records with low confidence are re-extracted because later events carry more complete metadata.</p>
<p>A second problem emerged during bulk operations. Tagging fifty documents simultaneously triggered approximately 300 webhook events in seconds. Without concurrency control, the system attempted 300 simultaneous AI calls, saturating the model server and leaving the event loop unable to respond to any other requests. A concurrency limiter restricts extractions to match the model server's actual capacity, with tasks queuing in application memory rather than flooding the model.</p>
<h3>LLM Output Defensiveness</h3>
<p>Every LLM integration in a production system requires explicit handling for the failure modes the model exhibits in normal use. The extraction pipeline encountered two recurring patterns: the model occasionally returns a list containing the extracted object rather than the object directly, causing downstream code to fail; and the model sometimes truncates its output at the token limit mid-string, producing unparseable output. Both require explicit recovery logic. A failed parse writes a placeholder record flagged for manual attention rather than silently dropping the document.</p>
<p>The classifier prompt was also hardened after observing production misclassifications. Quotes and estimates were being classified as completed transactions because both involve financial figures. The distinction between an unexecuted financial proposal and a completed payment was made explicit in the prompt, with worked examples of each category.</p>
<h3>A SQL Aggregation Bug</h3>
<p>The financial dashboard query joined issues and transactions on their shared parent property in a single query. A property with four open issues and three recorded transactions produces twelve rows — a cartesian product. Every transaction was being counted four times, and the dashboard displayed financial totals that were multiples of the correct figures. Pre-aggregating transactions in a subquery before joining resolved this. A standard SQL pattern, but one that is easy to miss when building incrementally and adding joins over time.</p>
<h2>What the Stack Looks Like Now</h2>
<p>Seventeen containerised services. The query path for a user question:</p>
<p>•        LLM query analysis resolves entity references against the live catalogue, rewrites the query for clarity, identifies whether the query is exhaustive or specific, and extracts any date range or identifier mentioned</p>
<p>•        Exhaustive queries route to a paginated scroll with payload filters — no ranking ceiling, complete result sets</p>
<p>•        Specific queries run hybrid BM25 sparse and dense semantic search simultaneously, with server-side rank fusion producing approximately 20 candidates</p>
<p>•        Cross-encoder reranking scores every candidate against the query — skipped entirely when metadata filters have already narrowed the pool sufficiently</p>
<p>•        The top 5 reranked chunks reach the LLM with a guardrailed prompt that explicitly prohibits date substitution, prohibits volunteering unrequested information, and includes direct links to source documents</p>
<p>•        The response is streamed to the browser and rendered from raw text to formatted markdown on completion</p>
<p>The document ingest path:</p>
<p>•        Documents arrive in the document management system and are enriched by a vision-capable AI model for OCR, title generation, tagging, and custom field extraction</p>
<p>•        Each document change fires webhook events to three parallel services: vector indexing with embedded metadata, deadline and calendar event extraction, and financial document classification</p>
<p>•        Extracted financial records enter a human review queue; approved records populate a structured property database with financial year filtering and export</p>
<h2>Honest Observations</h2>
<p><strong>RAG quality is predominantly a data quality problem.</strong> The largest single improvement in query reliability came from embedding structured metadata into the vector index — not from upgrading the retrieval algorithm. The algorithm can only find what you put in front of it.</p>
<p><strong>LLMs require explicit failure mode handling.</strong> Every LLM integration in this system required targeted prompt engineering against specific failure patterns observed in production: date blending, type misclassification, malformed output structure, hallucinated entity names, truncated output. These are not edge cases. They appear in normal daily use.</p>
<p><strong>Rules-based logic accrues maintenance debt rapidly.</strong> The fuzzy entity resolution system required a code change for every new query pattern it had not seen. The LLM-native replacement has required zero code changes for new patterns since deployment. The maintenance cost difference compounds over time.</p>
<p><strong>Human review is the right default for structured data extraction.</strong> Committing AI-extracted financial data directly to a database creates errors that compound silently. A review queue with good edit tooling is minimal additional friction and eliminates an entire category of data quality risk.</p>
<p><strong>Parallelism reveals capacity assumptions that sequential processing hides.</strong> The backfill coverage gap only appeared when running sixteen concurrent workers. At one document per second, the backend handled the load without issue. Parallel failure modes tend to be silent data gaps, not visible errors.</p>
<p>The home lab remains the best learning environment: real hardware, real data, real failure modes, no safety net. The engineering problems encountered here are exactly the problems that appear at production scale. Building against real constraints is the only way to encounter them.</p>
<h2>What's Next</h2>
<p>Email ingestion via a self-hosted automation engine — whitelisted senders only, email bodies treated as text documents, attachments routed through the existing pipeline, calendar events extracted automatically from email content. The existing RAG pipeline, extraction service, and calendar integration require no changes — email content flows through them automatically once it is present in the document system.</p>
<p>The core system is stable. The next phase is expanding what it ingests.</p>
<p><a href="http://herald-ai.hashnode.dev"><em>herald-ai.hashnode.dev</em></a></p>
<p><em>#AIInfrastructure #RAG #LLM #DocumentAI #Docker #VectorSearch #PrivateAI #HomeLab #MachineLearning #BM25 #RetrievalAugmentedGeneration</em></p>
]]></content:encoded></item><item><title><![CDATA[NVIDIA NIM vs Ollama — Which Should You Choose for Local LLM Deployment?]]></title><description><![CDATA[By Ravi Mohta
A hands-on story about building a local AI document intelligence stack, the promise of NVIDIA NIM, and what the RTX 50 series Blackwell architecture actually means for AI practitioners t]]></description><link>https://herald-ai.hashnode.dev/nvidia-nim-vs-ollama-which-should-you-choose-for-local-llm-deployment</link><guid isPermaLink="true">https://herald-ai.hashnode.dev/nvidia-nim-vs-ollama-which-should-you-choose-for-local-llm-deployment</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Ravi Mohta]]></dc:creator><pubDate>Thu, 12 Mar 2026 00:22:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b1ffd76c896b0519d10f41/7ab0859c-56c1-47f5-8da4-1c09ef611243.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>By Ravi Mohta</em></p>
<p><em>A hands-on story about building a local AI document intelligence stack, the promise of NVIDIA NIM, and what the RTX 50 series Blackwell architecture actually means for AI practitioners today.</em></p>
<hr />
<p>I spent today trying to run NVIDIA NIM containers on my home lab machine — an Acer Predator laptop with an RTX 5090 (24GB GDDR7), 192GB ECC RAM, running a full Paperless document management stack on Docker Desktop with WSL2 Ubuntu for Ollama underneath.</p>
<p>The short version: NIMs don't run on an RTX 5090 yet. And the reasons why are genuinely interesting.</p>
<p>But let me start at the beginning.</p>
<hr />
<h2>What I Was Actually Building</h2>
<p>I run Paperless-ngx at home for document management — utility bills, insurance documents, bank statements, the usual. On top of that I've built a small AI stack that uses an LLM to OCR my scanned documents, extract metadata, auto-title everything, and feed it all into a Qdrant vector store for RAG queries later.</p>
<p>The stack currently looks like this:</p>
<ul>
<li><p><strong>Paperless-ngx</strong> — document ingestion and management</p>
</li>
<li><p><strong>paperless-gpt</strong> — LLM-based OCR and document intelligence</p>
</li>
<li><p><strong>Ollama</strong> (WSL2 Ubuntu) — serving the LLMs, talking to paperless-gpt via Windows port forwarding</p>
</li>
<li><p><strong>Qdrant</strong> — vector database for RAG</p>
</li>
<li><p><strong>Custom RAG processor</strong> — webhook receiver that embeds documents on ingest</p>
</li>
</ul>
<p>It works well. But I wanted to compare NVIDIA NIM as a serving backend against Ollama — partly to understand the performance difference, partly because understanding the NVIDIA enterprise AI stack is genuinely useful for anyone working in this space professionally.</p>
<p>So I set up a second <code>docker-compose-nim.yml</code> — a completely separate NIM experiment stack that would run a second instance of paperless-gpt pointing at NIM instead of Ollama, differentiated by separate document tags so the two instances would never conflict.</p>
<p>Clean design. Good learning objective. Immediately hit interesting problems.</p>
<hr />
<h2>NIM vs Ollama — The Architecture Difference That Matters</h2>
<p>Before getting into what went wrong, it's worth understanding what NIM actually is, because it's meaningfully different from Ollama in ways that matter.</p>
<p><strong>Ollama</strong> is flexible. You run a server, pull any compatible model, swap between them with a single command. It uses llama.cpp under the hood, which handles quantisation and CPU offloading — so on my 192GB RAM machine, I can run a 70B model with most layers on GPU and overflow to RAM. The ceiling is effectively much higher than my 24GB VRAM alone would suggest.</p>
<p><strong>NIM</strong> is purpose-built. One container = one model. Each NIM image is compiled specifically for that model architecture and optimised for target GPU hardware using TensorRT-LLM or vLLM. There's no model swapping, no CPU offloading. The model must fit entirely in VRAM. What you trade away in flexibility you're supposed to gain in inference performance — better batching, paged attention, lower latency at scale.</p>
<p>For enterprise deployment on dedicated GPU infrastructure, this is the right trade-off. For a learning/dev environment on a laptop? The rigidity starts creating problems immediately.</p>
<hr />
<h2>Problem 1: GPU Display Memory</h2>
<p>First attempt — NIM failed with:</p>
<pre><code class="language-plaintext">Free GPUs: &lt;None&gt;
Non-free GPUs: NVIDIA GeForce RTX 5090 Laptop GPU [current utilization: 17%]
</code></pre>
<p>NIM requires a <em>free</em> GPU — one with essentially no VRAM consumed. My 5090 was in "NVIDIA GPU only" mode in the Acer Predator settings, meaning it was driving the display AND handling compute on the same GPU. The display output was consuming ~4GB of VRAM. That was enough for NIM to refuse to start.</p>
<p><strong>Fix:</strong> Switch the laptop to Optimus mode. In Optimus, the Intel iGPU handles display output and the 5090 is reserved purely for compute. After a reboot, NIM saw a free GPU and moved on.</p>
<p>This is a real architectural difference worth knowing: NIM is designed for dedicated GPU serving environments. It assumes the GPU isn't doing anything else. On a workstation or data centre node, this is always true. On a laptop where the GPU also drives the screen, you need to engineer around it.</p>
<hr />
<h2>Problem 2: The RTX 5090 Is Too New</h2>
<p>This is the more interesting failure, and the one with broader implications.</p>
<p>After switching to Optimus, NIM started — selected a profile (<code>vllm-bf16-tp1</code>), downloaded ~22GB of model files and runtime components, started the vLLM process, began loading the model... and then crashed with:</p>
<pre><code class="language-plaintext">NVIDIA GeForce RTX 5090 Laptop GPU with CUDA capability sm_120 is not compatible
with the current PyTorch installation.
The current PyTorch install supports CUDA capabilities sm_50 sm_60 sm_70 sm_75
sm_80 sm_86 sm_90.
RuntimeError: CUDA error: no kernel image is available for execution on the device
</code></pre>
<p>The RTX 5090 is Blackwell architecture — CUDA compute capability sm_120. Current stable PyTorch supports up to sm_90 (Ada Lovelace, i.e. RTX 4090 generation). NIM containers are built on stable PyTorch. Therefore: no NIM on RTX 5090.</p>
<p>This isn't specific to the model I was trying. It affects <strong>every current NIM container</strong> — because the PyTorch incompatibility is at the container base level, not the model level.</p>
<p>The irony? <strong>Ollama works fine on the same GPU.</strong> llama.cpp — the engine Ollama uses — added Blackwell CUDA support through a different compilation path, independently of PyTorch, and did so faster. So the older, more flexible tool runs on the newest hardware, while the enterprise-grade optimised serving platform can't.</p>
<hr />
<h2>What I Actually Learned About the NIM Stack</h2>
<p>Even though I couldn't get inference running, going through this process taught me things I wouldn't have understood from reading documentation:</p>
<p><strong>1. NIM's first-run process is substantial.</strong> It's not just pulling a Docker image. On first start, NIM performs GPU profile detection, downloads model weights separately from the container image (~15-22GB), and for supported GPU architectures, compiles TensorRT engines specific to your hardware. The downloaded artefacts are cached so subsequent starts are fast — but that first run is a genuine installation process, not just a container start.</p>
<p><strong>2. Profile selection is intelligent.</strong> NIM inspects your GPU and selects the best inference profile — TensorRT-LLM for supported architectures (pre-compiled engine, maximum performance), vLLM as fallback for others. In my case it selected <code>vllm-bf16-tp1</code> because there's no prebuilt TensorRT engine for Blackwell yet. This auto-selection is genuinely useful in enterprise contexts where you're deploying across heterogeneous GPU fleets.</p>
<p><strong>3. The GPU exclusivity model is a deliberate design choice.</strong> NIM's requirement for a dedicated, unshared GPU isn't a bug — it's the architecture. In production you'd provision dedicated GPU nodes for inference. The constraint only feels strange in a dev/learning environment where the same GPU is doing multiple things.</p>
<p><strong>4. Per-model EULA acceptance is part of the governance model.</strong> Having NGC Catalog permissions in your API key isn't sufficient to pull NIM containers. Each model requires explicit terms acceptance on the NGC website. This is NVIDIA's model governance approach — fine-grained control over which models your account can access. Worth knowing before you try to automate NIM deployments.</p>
<p><strong>5. NIM separates container from model artefacts.</strong> The Docker image contains the runtime framework. Model weights are downloaded at first start and cached separately. This means NVIDIA can update the runtime independently of the model, and you can switch GPU profiles without re-downloading weights. It's a cleaner architecture than bundling everything into a single image.</p>
<hr />
<h2>The Honest Comparison</h2>
<p>After today, here's where I'd actually characterise the two approaches:</p>
<p><strong>Ollama strengths:</strong></p>
<ul>
<li><p>Works on newer hardware faster (llama.cpp moves quicker than PyTorch stable)</p>
</li>
<li><p>CPU offloading — on a machine with 192GB RAM, I can run models much larger than VRAM alone</p>
</li>
<li><p>Model flexibility — swap models with an env var change</p>
</li>
<li><p>Lower friction for home/dev environments</p>
</li>
</ul>
<p><strong>NIM strengths (when it works):</strong></p>
<ul>
<li><p>TensorRT-LLM optimisation — meaningfully faster inference on supported hardware</p>
</li>
<li><p>Paged attention — more efficient KV cache management at long context lengths</p>
</li>
<li><p>Production-grade batching and concurrency</p>
</li>
<li><p>OpenAI-compatible API — drop-in replacement for application integration</p>
</li>
<li><p>Enterprise governance model built in</p>
</li>
</ul>
<p><strong>The key insight:</strong> Ollama and NIM aren't really competing for the same use case. Ollama is the right choice for development, experimentation, and home labs. NIM is designed for production serving on dedicated, enterprise-grade infrastructure. Trying to run NIM on a cutting-edge consumer laptop is a useful experiment precisely because it reveals these assumptions.</p>
<hr />
<h2>What's Actually Running on My RTX 5090 Right Now</h2>
<p>My Paperless stack is producing good results with Ollama and ministral-3:14b as the OCR model. I know from testing that 14B parameters is the floor for reliable OCR quality on my document types — smaller models consistently produce errors that make the whole pipeline unreliable.</p>
<p>The 5090 with 192GB system RAM means I could push to 70B models via Ollama's GPU+RAM offloading — something NIM's hard GPU-only constraint would prevent. That's an interesting architectural point: sometimes the "less optimised" serving infrastructure gives you more headroom in practice.</p>
<p>The RAG layer is next — a chat interface over my document store. And voice after that.</p>
<hr />
<h2>The Broader Point for Anyone Building on New Hardware</h2>
<p>If you're running RTX 50 series (Blackwell) hardware and trying to use PyTorch-based frameworks: you're in a gap period. The hardware shipped, drivers work, llama.cpp works, CUDA toolkit 12.8 supports sm_120 — but stable PyTorch doesn't, and everything built on stable PyTorch (vLLM, standard NIM containers) inherits that limitation.</p>
<p>The path forward is either:</p>
<ul>
<li><p>PyTorch nightly builds with cu128 support (less stable, more work)</p>
</li>
<li><p>Building custom containers from NVIDIA's PyTorch nightly base images</p>
</li>
<li><p>Waiting for stable PyTorch + NIM to catch up (probably a few months)</p>
</li>
</ul>
<p>This is normal early-adoption friction. It happened with every previous GPU architecture. But it's worth knowing before you invest time trying to run enterprise inference frameworks on day-one consumer hardware.</p>
<hr />
<h2>Takeaways</h2>
<p>This wasn't a day where everything worked. It was a day where I understood <em>why</em> things didn't work, which is usually more valuable.</p>
<p>I came away understanding NIM's architecture at a level I couldn't have reached from documentation alone — the profile selection system, the separation of container and model artefacts, the GPU exclusivity model, the governance layer, and exactly where the Blackwell compatibility gap sits in the stack.</p>
<p>The home lab is the best kind of learning environment: real hardware, real constraints, real failure modes. No sandbox, no pre-configured environment, no safety net. Just docker logs and patience.</p>
<p>Next up: RAG chat interface and voice. More soon.</p>
<hr />
<p><em>Building this on: Acer Predator | RTX 5090 Laptop 24GB | 192GB ECC RAM | Docker Desktop | WSL2 | Paperless-ngx | Ollama | Qdrant</em></p>
<p><em>Tags: #NVIDIA #NIM #Ollama #LLM #RAG #AIInfrastructure #MachineLearning #HomeLab #Docker #RTX5090 #Blackwell</em></p>
]]></content:encoded></item></channel></rss>