Skip to content
Running AI On-Prem: A Practical Guide to Using Local LLMs (like Phi-3 & Llama 3) in .NET

Running AI On-Prem: A Practical Guide to Using Local LLMs (like Phi-3 & Llama 3) in .NET

Running AI On-Prem: A Practical Guide to Using Local LLMs (like Phi-3 & Llama 3) in .NET

Abstract

The rapid progress in Large Language Models (LLMs) has ushered in an era where AI is no longer a privilege exclusive to hyperscale clouds. For software architects working within regulated, high-security, or latency-sensitive environments, the prospect of running LLMs on-premises is no longer theoretical—it is an increasingly practical and strategic choice. This article is a definitive guide for .NET architects, principal engineers, and senior developers who want to operationalize state-of-the-art models such as Microsoft’s Phi-3 and Meta’s Llama 3 entirely within their own infrastructure.

We move past vendor hype and focus on actionable insights: from selecting the right hardware and understanding quantization, to orchestrating Retrieval-Augmented Generation (RAG) applications with LLamaSharp, ONNX Runtime, and Microsoft.SemanticKernel. You’ll find clear architectural patterns, practical code samples (using .NET 8+), and best practices for deploying robust, privacy-first AI solutions at enterprise scale. Whether you’re aiming for compliance with strict data residency laws, or simply seeking to control costs and customize at will, this guide aims to help you make informed decisions and build production-ready systems.


1 The Strategic Imperative: Why On-Premise AI is the Next Frontier for the Enterprise

1.1 Beyond the Public Cloud: Redefining the Business Case for Local LLMs

For most organizations, public cloud APIs have been the fastest route to generative AI pilots. But as use cases mature, and as language models become more efficient, forward-thinking teams are discovering compelling reasons to bring AI on-premises.

1.1.1 Data Sovereignty and Ultimate Privacy

Many industries face strict requirements around data residency and handling. If you process sensitive data—think personally identifiable information (PII), protected health information (PHI), financial records, or proprietary source code—the cloud’s shared-responsibility model can introduce significant risk.

By running LLMs on your own hardware, sensitive data never leaves your network perimeter. You retain full control over inputs, outputs, and logs. For regulated verticals such as healthcare, finance, and critical infrastructure, this capability isn’t just a feature—it’s often a requirement for legal compliance.

1.1.2 Cost Predictability and Control

Cloud-based LLM APIs are priced per token, often with opaque, variable pricing. As usage grows, costs can become unpredictable—sometimes dramatically so. On-prem AI shifts your costs to a fixed hardware investment (and manageable energy/maintenance), offering predictable total cost of ownership (TCO).

Are you supporting high-frequency tasks, like document summarization or internal code search? You’ll appreciate knowing exactly what your spend is, month after month, regardless of spikes in usage.

1.1.3 Ultra-Low Latency

Network round-trips introduce unavoidable latency—often 200ms or more per request, even with the best APIs. For interactive applications such as chatbots, code completion, or real-time language agents, this can be a show-stopper.

With models running locally, inference happens at memory and compute speeds. End-to-end latency often drops below 50ms, enabling use cases previously out of reach.

1.1.4 Customization and Control

Relying on a cloud provider’s roadmap often means waiting for features, living with arbitrary limits, and worrying about model deprecation. Running your own models allows for deep customization: fine-tuning, prompt engineering, and even patching model weights to address bias or compliance issues.

Control extends to deployment strategies. Want to A/B test multiple models? Isolate users for red-team exercises? Or simply avoid vendor lock-in? On-prem AI puts you in the driver’s seat.

1.1.5 Offline Capability

Some applications must run in air-gapped or intermittently connected environments: think manufacturing plants, oil rigs, defense installations, or field operations. On-prem models work even when the internet doesn’t, ensuring business continuity.

1.2 The Rise of the SLM (Small Language Model): How Models like Phi-3 Make On-Prem Viable

A few years ago, local LLMs were mostly academic curiosities. Modern “small” models—ranging from 3 billion to 8 billion parameters—are now efficient enough to run on a single GPU or even powerful CPUs, without dramatic compromises in output quality.

1.2.1 Good Things, Small Packages

Phi-3 (from Microsoft) and Llama 3 (from Meta) have upended the notion that only giant models can be useful. Benchmarks regularly show that well-trained 3B to 8B parameter models, especially instruction-tuned variants, outperform older 30B+ parameter models on many practical tasks.

Their smaller size means less hardware, lower power, and reduced operational complexity. For most enterprise use cases—summarization, code search, document Q&A, and virtual agents—these models deliver high accuracy with impressive efficiency.

1.2.2 The Democratization of AI Hardware

A few years ago, “running AI locally” implied racks of expensive GPUs. Today, commodity workstations, desktops, or small clusters can handle surprisingly powerful models. This democratization means even mid-size enterprises or departmental teams can deploy on-prem AI without massive capital outlay.


2 Foundations: Deconstructing the Local LLM Stack

Before you build, you need to know what’s under the hood. Let’s break down the core concepts and components every .NET architect should understand.

2.1 The Modern LLM Landscape: A Briefing for Architects

2.1.1 Phi-3 Family (Microsoft): The “small, smart” choice

Phi-3 is Microsoft’s flagship Small Language Model series, optimized for high performance and accuracy in a compact package. Available in variants like phi3:mini, phi3:small, and phi3:medium (3.8B, 7B, and 14B parameters, respectively), these models are tuned to punch above their weight.

  • Phi-3-mini (3.8B): Highly efficient, suitable for CPUs and lightweight GPUs. Excellent for chatbots, basic RAG, and simple completion tasks.
  • Phi-3-small (7B): A sweet spot for quality and hardware needs. Performs well on reasoning and multi-turn conversations.
  • Phi-3-medium (14B): For teams with stronger hardware needs who want state-of-the-art results across a wide range of tasks.

2.1.2 Llama 3 Family (Meta): The open-source powerhouse

Meta’s Llama 3 series, especially the 8B and 70B parameter instruction-tuned models, has set a new bar for open-source LLMs. Llama-3-8B-Instruct delivers high-quality responses, context retention, and broad multi-domain knowledge—all within reach for on-prem workloads.

  • Llama-3-8B-Instruct: A widely adopted default for enterprise experimentation.
  • Llama-3-70B-Instruct: Exceptional performance but requires significant hardware (multiple GPUs or high-end servers).

2.1.3 Base vs. Instruction-Tuned Models

For most applications, you’ll want an “Instruct” or “Chat” variant. These models are further trained to follow user instructions, respond to conversational prompts, and behave as helpful agents—critical for RAG, chatbots, and internal assistants. Base models, in contrast, are best reserved for research or fine-tuning.

2.2 The Key to Efficiency: Understanding Model Quantization

Large models are computationally intensive, both in RAM and GPU/CPU needs. Quantization makes running them practical on commodity hardware.

2.2.1 What is Quantization? From 16-bit Floats to 4-bit Integers

Quantization reduces the numerical precision of a model’s parameters, shrinking their size and speeding up inference. Standard models use 16-bit floating-point numbers (FP16), but quantized models may use 8-bit, 5-bit, or even 4-bit integers.

  • FP16 (16-bit float): Baseline for most research and GPU training. High memory and compute requirements.
  • INT8 (8-bit integer): Big reduction in memory; minor drop in quality.
  • Q5, Q4, Q3 (5-, 4-, 3-bit): Maximum efficiency for inference, with quantization-aware training or smart rounding to retain as much accuracy as possible.

2.2.2 GGUF (The de facto standard): The magic behind LLamaSharp’s simplicity

GGUF is a file format for quantized models, widely used in the open-source LLM community. It enables ultra-efficient model loading and inference. Popular quantization variants include:

  • Q4_K_M: 4-bit, very fast, suitable for most chat and RAG applications.
  • Q5_K_M: 5-bit, slightly larger, but with improved accuracy.
  • Q8_0: 8-bit, closest to full-precision performance.

Each quantization level represents a trade-off: lower precision means more speed and smaller RAM footprint, but potentially reduced answer quality. For many enterprise applications, Q4 or Q5 strikes a practical balance.

2.2.3 ONNX Quantization: A more framework-agnostic approach

ONNX (Open Neural Network Exchange) is a standard for representing machine learning models. Quantized ONNX models can be used across frameworks (PyTorch, TensorFlow, .NET), and ONNX Runtime provides optimized inference on CPUs and GPUs. For .NET teams, this offers a flexible, cloud-agnostic path to deploying models, including those exported from Hugging Face or custom-trained models.

2.3 The .NET Toolbox for Local AI

2.3.1 LLamaSharp: The most direct path

LLamaSharp is a C# library that wraps the high-performance llama.cpp engine. It makes working with GGUF models in .NET seamless, with both CPU and GPU support. Ideal for rapid prototyping and production workloads alike.

2.3.2 ONNX Runtime: The universal inference engine

ONNX Runtime is Microsoft’s cross-platform engine for running ONNX models. It supports both CPU and GPU execution, quantized models, and advanced features like session optimization and hardware acceleration (e.g., DirectML on Windows, CUDA on Linux).

2.3.3 Microsoft.SemanticKernel: The orchestrator

Semantic Kernel is Microsoft’s orchestration layer for AI, designed to compose, manage, and chain prompts, planners, and memory. It supports local LLMs as well as cloud APIs, making it easy to swap model backends, integrate RAG, and build intelligent agents.


3 The Workbench: Preparing Your .NET Environment for Local Inference

Transitioning from cloud-based AI to on-prem LLMs is less about a single tool and more about orchestrating a reliable, repeatable stack that supports robust, production-grade inference. Before writing your first line of code, it’s critical to build a workbench tailored to the demands of modern language models. This section breaks down hardware sizing, environment preparation, and best practices for model management.

3.1 Hardware Is King: Sizing Your On-Prem AI Server

The single most common pitfall for teams new to on-prem LLMs is underestimating the hardware required for smooth, responsive inference. Model size, context length, quantization level, and intended throughput all play a role. Let’s examine what matters most.

3.1.1 CPU-Only Inference: Feasible for Testing, Not for Production

Running a quantized SLM like Phi-3-mini on a modern multi-core CPU is surprisingly achievable—especially at Q4 or Q5 quantization. This is an excellent way to validate model loading, integration, and pipeline orchestration. However, even a well-optimized CPU implementation is outclassed by GPU inference for anything beyond light, ad hoc workloads.

Consider CPU-only inference as your “unit test” environment. For production, especially with multiple users or interactive agents, you’ll want more muscle.

Realistic Expectations with CPUs
  • Good for: Model exploration, prototyping, debugging, or applications with infrequent, small requests.
  • Limitations: Throughput collapses under load. Multi-second latency for even modest completions.
  • Tip: Choose a CPU with high single-threaded performance and AVX-512 support if possible.

3.1.2 GPU-Accelerated Inference: The Gold Standard

For any LLM application with expectations of speed, responsiveness, or scale, GPU acceleration is the gold standard. Let’s break down the two main players in the ecosystem:

NVIDIA (CUDA): The Most Mature Ecosystem

NVIDIA’s CUDA stack is the default choice for local AI. Virtually all performance-optimized inference libraries (llama.cpp, ONNX Runtime, Hugging Face Transformers, etc.) offer first-class CUDA support.

  • Driver Setup: Make sure you’ve installed the latest CUDA toolkit and drivers from NVIDIA’s site.
  • Verification: Use nvidia-smi to verify your GPU(s) are detected and available.
  • Memory Management: NVIDIA cards from the RTX 3060 upwards (ideally 12GB VRAM or more) are ideal for 7B-8B parameter models.
AMD (ROCm): A Viable Alternative

AMD’s ROCm stack has seen rapid improvements, making high-end Radeon cards increasingly attractive, especially for teams looking to diversify hardware or control costs.

  • Support: Check that your chosen library supports ROCm. For .NET, llama.cpp (and thus LLamaSharp) offers growing ROCm support.
  • Verification: The rocminfo tool helps confirm setup.
Quick Comparison
  • NVIDIA CUDA: Best-in-class performance and compatibility. Widest library support.
  • AMD ROCm: Excellent price-to-performance for compatible cards. Slightly less mature software ecosystem but closing the gap.

3.1.3 RAM vs. VRAM: Calculating What You Need

Sizing your machine is simple in theory, but critical to get right in practice. LLMs are memory-hungry, and both RAM (for CPU inference) and VRAM (for GPU inference) can bottleneck your deployment.

Practical Formula
Required VRAM/RAM = Quantized Model File Size (GB) + [Context Length (tokens) × 2 bytes/token × Num Concurrent Sessions / 1024^3]
  • Quantized Model Size: A Q4 quantized 7B model is roughly 4-6GB.
  • Context Size: Each token in the context (prompt + output) occupies additional memory. For a 4096-token context, reserve an extra 1GB.
  • Concurrent Sessions: For multi-user apps, multiply context memory by max simultaneous users.
Example Calculation
  • Model: Llama-3-8B Q4_K_M (5GB)
  • Context: 4096 tokens/session (~1GB)
  • Concurrent Users: 4
Total VRAM needed = 5GB (model) + (1GB × 4) = 9GB

So, an RTX 4070 (12GB VRAM) can comfortably support this load.

3.1.4 Example Setups: From Developer Desktop to Departmental Server

Entry Level: Developer Desktop
  • GPU: RTX 4060/4070 (8–12GB VRAM)
  • CPU: 8+ core Ryzen or i7/i9
  • RAM: 32GB (to handle background tasks and caching)
  • Use case: Solo development, basic chatbot, prototype RAG
Mid-Tier: Departmental/Small Team Server
  • GPU: RTX 4090 (24GB VRAM) or NVIDIA A100 (40–80GB, for larger models)
  • CPU: Dual-socket Xeon or Threadripper
  • RAM: 128GB+
  • Use case: Multi-user apps, internal agents, moderate document RAG
Production-Grade: Enterprise Deployment
  • GPUs: Multiple A100/H100s (80GB+), or multi-node clusters with NVLink
  • CPU: High core count server-grade processors
  • RAM: 512GB+
  • Use case: High concurrency, large contexts, massive document corpora, or real-time services

Tip: Always provision extra headroom. LLMs scale best when hardware is not the bottleneck.

3.2 Software and Model Preparation

With hardware in place, your next focus is the environment setup. .NET’s flexibility means you can choose between the highly-optimized LLamaSharp stack and the broader compatibility of ONNX Runtime.

3.2.1 Installing .NET 8+ and Necessary SDKs

.NET 8 brought major improvements for performance, native AOT, and cross-platform support—all highly relevant for on-prem AI workloads.

  • Install .NET 8 SDK: On Windows/macOS/Linux: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
  • Verify: Run dotnet --info to confirm installation.
Optional: Hardware Acceleration Dependencies
  • NVIDIA: Install CUDA Toolkit and cuDNN.
  • AMD: Install ROCm as per your OS and GPU model.

3.2.2 A Note on Python

While you can stay in .NET for inference and orchestration, Python often sneaks in for one-time tasks like model conversion, quantization, or advanced fine-tuning. Don’t fight this—embrace it as part of a healthy toolchain.

  • For conversion: Tools like transformers, optimum, and gguf often require Python 3.9+ and pip-installed dependencies.
  • Best Practice: Use virtual environments to avoid package conflicts.

3.2.3 Model Acquisition: Securely Downloading from Hugging Face

Hugging Face’s Model Hub is the central repository for open LLM weights, including official and community-quantized Phi-3 and Llama 3 variants. Security and provenance matter, especially for enterprise deployments.

  • Install CLI: pip install huggingface_hub

  • Login: huggingface-cli login

  • Download Model:

    huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct-GGUF --local-dir ./models/llama-3-8b
  • Verify Integrity: After download, always verify checksums to ensure you have uncorrupted, unaltered files.

    sha256sum llama-3-8b-instruct-q4_K_M.gguf

4 The First Step: A “Hello, World!” with a Local LLM in .NET

With hardware humming and models ready, let’s build a minimal working example to demonstrate the fundamentals. This not only proves your stack but acts as a template for future enhancements.

LLamaSharp is purpose-built for .NET developers, offering a direct bridge to the high-performance llama.cpp engine. It abstracts away low-level details and enables rapid iteration.

4.1.1 Project Setup: Creating a Console App

First, spin up a new .NET 8 console app and add the LLamaSharp NuGet package.

dotnet new console -n LocalLLMHelloWorld
cd LocalLLMHelloWorld
dotnet add package LLamaSharp

4.1.2 Loading Your First Model

Let’s instantiate the model, pointing to your GGUF file.

using LLama;
using LLama.Common;

// Path to your GGUF model
var modelPath = @"C:\models\llama-3-8b-instruct-q4_K_M.gguf";
var modelParams = new ModelParams(modelPath)
{
    ContextSize = 2048,   // Adjust as needed
    GpuLayerCount = 30    // For RTX 4070+, set to use most VRAM
};
var model = LLamaModel.Load(modelParams);

4.1.3 Configuring the Context

Model parameters are critical. Tuning ContextSize (max tokens in prompt + response) and GpuLayerCount (how many layers run on GPU vs. CPU) allows you to balance speed, quality, and hardware constraints.

  • ContextSize: Most LLMs support up to 4k tokens (some up to 8k or 32k). Increase for RAG or long documents.
  • GpuLayerCount: Use as many as your GPU’s VRAM allows for max speed.

4.1.4 Running Inference: Simple Stateful Conversation

Wrap your model in a chat session for conversational memory.

var session = new ChatSession(model);
Console.WriteLine("Say hello to your local LLM!");

while (true)
{
    Console.Write("User: ");
    var userInput = Console.ReadLine();
    if (userInput?.ToLower() == "exit") break;

    var response = await session.ChatAsync(userInput);
    Console.WriteLine($"LLM: {response}");
}

4.1.5 Streaming for Responsiveness

LLM inference can feel sluggish if you wait for an entire response. LLamaSharp supports token streaming, enabling “typewriter effect” output.

await foreach (var token in session.StreamChatAsync(userInput))
{
    Console.Write(token);
}
Console.WriteLine();

This dramatically improves the user experience, especially in interactive settings.

4.2 Path B: The ONNX Runtime Approach (For Universal Deployment)

ONNX Runtime is your choice if you want maximum model compatibility (including converted Hugging Face models) and hardware flexibility.

4.2.1 Project Setup and Model Conversion

  • Install ONNX Runtime NuGet:

    dotnet add package Microsoft.ML.OnnxRuntime
  • Convert Your Model: If starting from a PyTorch model, use transformers or optimum in Python to export to ONNX.

    from transformers import AutoModelForCausalLM, AutoTokenizer
    import torch
    
    model = AutoModelForCausalLM.from_pretrained('meta-llama/Llama-3-8B-Instruct')
    tokenizer = AutoTokenizer.from_pretrained('meta-llama/Llama-3-8B-Instruct')
    
    # Example: Export single input
    inputs = tokenizer("Hello, world!", return_tensors="pt")
    torch.onnx.export(model, (inputs['input_ids'],), "llama3.onnx", ...)

4.2.2 Building the Inference Pipeline

ONNX doesn’t handle tokenization or output decoding natively—you need to manage these in .NET or with helper libraries.

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
// Assume you've handled tokenization elsewhere

using var session = new InferenceSession("llama3.onnx");
var inputTensor = new DenseTensor<long>(inputIds, new int[] {1, inputIds.Length});
var inputs = new List<NamedOnnxValue>
{
    NamedOnnxValue.CreateFromTensor("input_ids", inputTensor)
};

using IDisposableReadOnlyCollection<DisposableNamedOnnxValue> results = session.Run(inputs);
// Extract logits, decode tokens, etc.

4.2.3 Comparing the Complexity

While ONNX is immensely flexible, building an end-to-end chat or RAG pipeline is considerably more involved than with LLamaSharp. You’ll need to:

  • Implement or integrate tokenization (potentially using Hugging Face Tokenizers in a .NET-compatible wrapper)
  • Manage input shaping and padding
  • Decode output tokens into human-readable text

In practice:

  • LLamaSharp is best for rapid prototyping and production, as it provides a tight, idiomatic .NET experience.
  • ONNX Runtime is your go-to for custom models, hybrid scenarios, or when Python-based model export is part of your workflow.

5 The Core Pattern: Architecting for Retrieval-Augmented Generation (RAG)

As local LLMs become viable in the enterprise, it’s Retrieval-Augmented Generation (RAG) that consistently emerges as the real differentiator. While out-of-the-box LLMs are strong at language and reasoning, they have well-documented weaknesses that can hinder business value. RAG addresses these gaps by systematically grounding model responses in your private, trusted data.

5.1 Why RAG is the Killer App for Enterprise AI

5.1.1 The Problem: Hallucinations and Out-of-Date Knowledge

Even state-of-the-art models like Phi-3 or Llama 3 are fundamentally limited by their training cut-off and lack of direct access to your proprietary information. They might generate fluent but factually incorrect responses (hallucinations), misquote internal procedures, or miss recent policy changes.

  • Example: Ask a base Llama 3 about your Q2 2025 business results, and it will invent plausible-sounding but inaccurate answers—because it doesn’t have access to your latest documents.

5.1.2 The Solution: Grounding the LLM with Your Company’s Private Data

RAG works by dynamically fetching the most relevant snippets from your enterprise data and injecting them into the LLM’s prompt. This grounds the model’s answer, reducing hallucination and increasing verifiability. Answers can be linked to specific documents or data points, supporting audits and compliance.

  • Outcome: Your chatbot, knowledge assistant, or support tool responds with accurate, up-to-date information and clear references to where the answer originated.

5.2 RAG Architectural Blueprint

A robust RAG system is modular and decoupled, blending offline ingestion with real-time retrieval and synthesis. Let’s lay out the core pipelines.

5.2.1 The Ingestion Pipeline (Offline)

The ingestion pipeline operates asynchronously, transforming raw company data into a searchable format for real-time queries.

  1. Data Source: Fileshares, SharePoint, document management systems, code repositories, ticket systems.
  2. Document Loader: Reads files in various formats—PDFs, DOCX, Markdown, CSV, HTML.
  3. Text Chunker: Splits documents into digestible, semantically coherent pieces (e.g., paragraphs, sections) of optimal token length.
  4. Embedding Model: Converts each chunk into a vector representation using a local model (ONNX or GGUF).
  5. Vector Database: Stores the vectors, document metadata, and chunk text for fast similarity search.

5.2.2 The Query Pipeline (Online)

This is the real-time path for answering user questions.

  1. User Query: Typed, spoken, or API-based question.
  2. Embedding Model: Converts the query to a vector, same as in ingestion.
  3. Vector DB Search: Finds the most semantically similar document chunks.
  4. Retrieved Context: The top-N relevant snippets.
  5. Augmented Prompt: Combines the user’s question with retrieved context.
  6. LLM: Generates a grounded answer using the context as its knowledge base.
  7. Synthesized Answer: Returns the answer, often with source citations.

5.2.3 Visual Blueprint

Picture this as two pipelines:

[Offline Ingestion]         [Online Query]
Data → Loader → Chunker     User Query
     → Embedding →         → Embedding →
     Vector DB             Vector DB Search
                           → Retrieve Chunks
                           → Build Prompt
                           → LLM → Answer

This separation allows the heavy-lifting (document parsing and embedding) to be done ahead of time, ensuring real-time performance when answering questions.

5.3 Choosing Your RAG Components

5.3.1 Vector Databases

Your vector store must support efficient nearest-neighbor search, strong durability, and—ideally—simple integration with .NET. Some leading options:

  • Qdrant: Open-source, excellent .NET client, fast, supports payload metadata and filtering.
  • Milvus: Highly scalable, suitable for massive corpora, robust ecosystem, but requires more operational overhead.
  • Weaviate: Rich semantic features, hybrid search, Python-first but workable via REST.
  • Microsoft.SemanticKernel.Connectors.Memory: In-memory or file-backed, integrated with Semantic Kernel, ideal for smaller setups or prototyping.

5.3.2 Embedding Models

An effective RAG setup depends on a performant embedding model that is small, fast, and accurate for your language/domain.

  • MiniLM, E5, Instructor: All available as quantized ONNX models, delivering good performance on CPUs and moderate GPUs.
  • Phi-3 Embedding Variants: Where available, offer excellent quality in a compact size, easily deployable via ONNX Runtime.
  • Key factors: Favor local models over cloud APIs for privacy, speed, and cost reasons.

6 Practical RAG Implementation in .NET

Now we translate architecture into code. This section walks through a full, production-style RAG solution—covering both ingestion and query pipelines—using modern .NET, local ONNX models, and Qdrant as the vector store. Concepts apply equally to other models or vector DBs.

6.1 Building the Ingestion Service

This service runs on a schedule or when new data arrives, processing documents for later retrieval.

6.1.1 Loading and Chunking Documents

Use robust .NET libraries to extract and split text.

using UglyToad.PdfPig; // For PDFs
using Markdig;         // For Markdown

// Example: PDF loading
var textChunks = new List<string>();
using (var pdf = PdfDocument.Open("annual_report.pdf"))
{
    foreach (var page in pdf.GetPages())
    {
        var text = page.Text;
        textChunks.AddRange(ChunkText(text, maxTokens: 512));
    }
}

// Example: Markdown loading
var markdown = File.ReadAllText("handbook.md");
var plainText = Markdown.ToPlainText(markdown);
textChunks.AddRange(ChunkText(plainText, maxTokens: 512));

// Chunking utility
List<string> ChunkText(string text, int maxTokens)
{
    // Split by paragraph, section, or use a token counter for LLMs
    var paragraphs = text.Split("\n\n");
    // Further logic to merge small chunks, split large ones, etc.
    return paragraphs.ToList();
}

6.1.2 Generating Embeddings Using a Local ONNX Model

Assume an ONNX embedding model (e.g., E5-base) is available.

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

var session = new InferenceSession("e5-base-v2.onnx");

float[] GetEmbedding(string text)
{
    // Pre-tokenize text (use a tokenizer library)
    var tokens = Tokenize(text); // Returns int[]
    var inputTensor = new DenseTensor<int>(tokens, new[] {1, tokens.Length});
    var inputs = new List<NamedOnnxValue>
    {
        NamedOnnxValue.CreateFromTensor("input_ids", inputTensor)
    };

    using var results = session.Run(inputs);
    var embeddingTensor = results.First().AsEnumerable<float>().ToArray();
    return embeddingTensor;
}

6.1.3 Writing the Vectors and Metadata to Qdrant

Using the official Qdrant .NET client (or REST API):

using Qdrant.Client;

var qdrant = new QdrantClient("http://localhost:6333");

for (int i = 0; i < textChunks.Count; i++)
{
    var vector = GetEmbedding(textChunks[i]);
    var payload = new Dictionary<string, object>
    {
        {"document_id", "annual_report_2025"},
        {"chunk_id", i},
        {"text", textChunks[i]}
    };

    await qdrant.UpsertAsync("enterprise-docs", new[]
    {
        new PointStruct
        {
            Id = Guid.NewGuid().ToString(),
            Vector = vector,
            Payload = payload
        }
    });
}

6.2 Building the Query Service

This is the online service—an ASP.NET Core API—that receives user questions, fetches relevant data, and synthesizes answers.

6.2.1 Creating an ASP.NET Core API Endpoint

Define a controller for queries.

[ApiController]
[Route("api/rag")]
public class RagController : ControllerBase
{
    [HttpPost("ask")]
    public async Task<IActionResult> Ask([FromBody] RagQuery query)
    {
        var answer = await _ragService.AnswerAsync(query.Question);
        return Ok(new { answer });
    }
}

6.2.2 Implementing the RAG Query Logic

public class RagService
{
    private readonly QdrantClient _qdrant;
    private readonly InferenceSession _embeddingSession;
    private readonly LLamaModel _llamaModel;

    public async Task<string> AnswerAsync(string userQuestion)
    {
        var queryEmbedding = GetEmbedding(userQuestion); // as before

        // Search top-3 similar chunks
        var searchResults = await _qdrant.SearchAsync(
            "enterprise-docs",
            queryEmbedding,
            limit: 3
        );

        // Concatenate retrieved chunks
        var context = string.Join("\n---\n", searchResults.Select(r => r.Payload["text"]));
        var augmentedPrompt = BuildPrompt(userQuestion, context);

        // Call LLM
        var session = new ChatSession(_llamaModel);
        var answer = await session.ChatAsync(augmentedPrompt);
        return answer;
    }

    string BuildPrompt(string question, string context)
    {
        return
$@"You are an expert company assistant. Use only the provided context to answer the question.
Context:
{context}
---
Question: {question}
Answer:";
    }
}

6.2.3 Constructing the Final, Context-Rich Prompt

Careful prompt construction is key—always include instructions to the model to only use the given context, reducing hallucination risk. Consider adding document citations.

// Example prompt:
"You are an enterprise assistant. Use only the below context to answer.
If you don't know, say so.
Context:
[chunk 1] [chunk 2] [chunk 3]
Question: What were our Q2 2025 sales?
Answer:"

6.3 Full Code Example: A Complete .NET RAG Solution

Below is a simplified, end-to-end example combining ingestion and query services. In production, you’d separate concerns, implement error handling, logging, authentication, and use async everywhere.

Program Entry

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSingleton<RagService>();
var app = builder.Build();

app.MapControllers();
app.Run();

Models

public class RagQuery
{
    public string Question { get; set; }
}

Ingestion (one-time setup or batch job)

// Load and chunk documents
var chunks = LoadChunksFromDocuments();
foreach (var chunk in chunks)
{
    var embedding = GetEmbedding(chunk.Text);
    await qdrant.UpsertAsync("enterprise-docs", new[]
    {
        new PointStruct
        {
            Id = chunk.Id,
            Vector = embedding,
            Payload = new Dictionary<string, object>
            {
                {"source", chunk.Source},
                {"text", chunk.Text}
            }
        }
    });
}

Query Controller

[ApiController]
[Route("api/rag")]
public class RagController : ControllerBase
{
    private readonly RagService _service;
    public RagController(RagService service) => _service = service;

    [HttpPost("ask")]
    public async Task<IActionResult> Ask([FromBody] RagQuery query)
    {
        var answer = await _service.AnswerAsync(query.Question);
        return Ok(new { answer });
    }
}

RagService (abbreviated)

public class RagService
{
    // Assume dependency-injected Qdrant, ONNX session, and LLM
    public async Task<string> AnswerAsync(string userQuestion)
    {
        // Steps as in 6.2.2
    }
}

7 Performance Optimization and Scaling

While achieving a functional RAG application is a major milestone, delivering real business value at scale requires a continuous focus on throughput, responsiveness, and robustness. Production-grade AI workloads must efficiently support simultaneous users, maintain predictable latency, and adapt to evolving demand. Performance engineering is not a single task, but a set of ongoing practices spanning code, architecture, hardware, and prompt design.

7.1 Maximizing Throughput: From Single to Multiple Users

7.1.1 Measuring Performance: TTFT and TPS

Before optimizing, measure where you stand. Two core metrics are especially meaningful for LLM-powered applications:

  • Time to First Token (TTFT): The elapsed time from a user submitting a prompt to the model generating the first word. This is a direct indicator of perceived responsiveness. For interactive experiences, aim for TTFT below 1 second.
  • Tokens per Second (TPS): The sustained rate at which the model generates output tokens. Higher TPS means shorter waits for full answers, which is vital for long-form content, summarization, or document generation.

How to Measure: Instrument your API endpoints and model invocation methods. Log timestamps before and after LLM calls. In LLamaSharp, hook into streaming token events and record precise timings.

var stopwatch = Stopwatch.StartNew();
await foreach (var token in session.StreamChatAsync(prompt))
{
    if (stopwatch.ElapsedMilliseconds == 0)
        Console.WriteLine($"TTFT: {stopwatch.ElapsedMilliseconds} ms");
    // ...handle token...
}
// At end, compute TPS.

7.1.2 Batching Requests: Grouping User Queries

GPUs are designed to maximize throughput when handling parallel work. Batching allows you to group several inference requests and process them together, amortizing overhead and making better use of available compute.

Batching in Practice:

  • Not all LLM inference libraries support automatic batching, but frameworks like ONNX Runtime (and some llama.cpp variants) allow you to stack multiple inputs as a single tensor.
  • In .NET, consider creating a “batcher” service that waits briefly (e.g., 10–50 ms) to collect incoming requests, then issues a batched call.

Example Pattern:

public class LlmBatcher
{
    private readonly Channel<(string Prompt, TaskCompletionSource<string> Result)> _queue;
    // ...setup...

    public async Task<string> QueueRequestAsync(string prompt)
    {
        var tcs = new TaskCompletionSource<string>();
        await _queue.Writer.WriteAsync((prompt, tcs));
        return await tcs.Task;
    }

    // Background task: Pulls from queue, batches, and submits to model
}

Trade-off: Shorter wait times mean smaller batches (lower throughput), while longer wait times increase latency but maximize GPU use. Find the right balance for your user experience.

7.1.3 Managing the Queue: Handling Concurrency

In any multi-user system, requests will arrive concurrently and at unpredictable rates. Robust request management prevents resource starvation and delivers fair, reliable responses.

.NET Solution: Implement a background queue with Channel<T> and a hosted service (IHostedService). This approach decouples incoming HTTP requests from model inference, providing smoothing and backpressure.

public class LlmHostedService : BackgroundService
{
    // Channel logic as above...

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Gather a batch, process, handle results...
        }
    }
}

This also allows for prioritization (e.g., premium users), load shedding, or intelligent scaling as demand grows.

7.2 Prompt Engineering for Local Models

Unlike the largest cloud models, compact SLMs (like Phi-3-mini or Llama-3-8B) are more sensitive to how you phrase your requests. Prompt engineering is both art and science—here, clarity and conciseness are your allies.

7.2.1 Less is More: Concise Prompts Yield Better Results

Small models have limited context windows and can become confused by verbose or ambiguous instructions. Avoid unnecessary verbosity; focus on direct, unambiguous tasks.

  • Ineffective: “You are a helpful assistant with extensive knowledge of enterprise sales and operations. Please provide a detailed summary…”
  • Effective: “Summarize the following context for Q2 2025 sales. Use only the facts provided.”

Split multi-part tasks into separate steps where possible. This not only helps the model but also simplifies debugging and traceability.

7.2.2 The Impact of System Prompts

A system prompt sets the tone and rules for the model. With local models, you control the initial context completely.

  • Use cases: Enforcing compliance, controlling answer style, constraining to context-only responses.

  • Example:

    "You are an enterprise knowledge assistant. Only use the provided context. If unsure, say 'I don't know based on the provided information.'"

Test variations; even minor tweaks can have outsized effects on accuracy, tone, or risk of hallucination.

7.3 Optimizing GPU Layer Offloading in LLamaSharp

Quantized models allow you to choose how many layers are executed on the GPU versus the CPU. This is a key tuning lever.

7.3.1 Finding the Sweet Spot

  • Full GPU Offload: Maximizes speed but may exceed VRAM on consumer GPUs (leading to out-of-memory errors).
  • Partial Offload: Only a subset of layers run on GPU; remaining are on CPU. This uses less VRAM, but at some cost to speed.

Tuning Strategy:

  1. Start with all layers on the GPU (GpuLayerCount = model.TotalLayers).
  2. If you hit memory errors, decrement the count until stable.
  3. For multi-user scenarios, leave headroom—never max out VRAM.
  4. Profile TTFT and TPS at each setting; chart the performance curve.

Sample Code:

var modelParams = new ModelParams(modelPath)
{
    ContextSize = 2048,
    GpuLayerCount = 30 // Tune this value
};

Monitor with system tools (nvidia-smi) to catch memory leaks or overuse. Regularly revisit as your user base and traffic patterns evolve.


8 Advanced Customization: A Practical Guide to Fine-Tuning

RAG is a flexible pattern that leverages your private data. Fine-tuning, meanwhile, adapts the model’s intrinsic capabilities—teaching it new skills, styles, or behaviors at the parameter level. Both are powerful; knowing when and how to use each is an essential architectural skill.

8.1 When to Fine-Tune: RAG vs. Fine-Tuning

8.1.1 RAG for Knowledge, Fine-Tuning for Skill or Behavior

  • Use RAG when: Your primary need is to keep answers current, link them to specific facts, and support traceability or compliance. RAG shines at Q&A, document retrieval, and context-specific reasoning—without touching the base model.
  • Use Fine-Tuning when: You require the model to perform a new type of task (e.g., code translation, sentiment analysis), follow a proprietary style, or exhibit unique conversational behaviors not achievable with prompting alone.

Fine-tuning “bakes in” new capabilities, while RAG “looks up” answers. In many enterprise settings, a combination yields the best results.

8.1.2 A Decision Tree for Architects

  • Is the requirement mainly about knowledge or recall?

    • Use RAG.
  • Is it about specific writing style, process, or skill?

    • Consider fine-tuning.
  • Does the application need both?

    • Combine: Use RAG for grounding, and a fine-tuned model for skill.

Examples:

  • RAG alone: “What’s our latest leave policy?”
  • Fine-tuning alone: “Rephrase text in our company’s writing style.”
  • Both: “Answer client questions in the tone of our support team, using only current support docs.”

8.2 An Introduction to LoRA (Low-Rank Adaptation)

LoRA (Low-Rank Adaptation) is a breakthrough in practical fine-tuning. It enables you to adapt powerful LLMs using far fewer resources—without retraining the full model.

8.2.1 What is LoRA?

LoRA works by injecting small “adapter” modules into select layers of the model, allowing targeted changes to model behavior. The main model weights remain frozen, and only a small set of new parameters is learned and stored.

Benefits:

  • Dramatically reduces hardware and time requirements.
  • Fine-tuned adapters are tiny (often just a few hundred MB).
  • Supports versioning: Easily swap, merge, or remove adapters for different tasks.

8.2.2 The Process Overview

A. Preparing a Dataset

You’ll need a task-specific dataset, formatted as pairs of input and desired output—this might be instructions and ideal completions, specific code snippets, or chat dialogues.

  • Use your domain data: customer service tickets, internal emails, or annotated QA pairs.
  • Clean and balance your data to avoid unwanted bias.

B. Running the Training (Python, with Hugging Face or LLaMA.cpp)

Most LoRA tools run in Python. Example (using Hugging Face PEFT):

from peft import get_peft_model, LoraConfig, TaskType
from transformers import AutoModelForCausalLM, Trainer, TrainingArguments

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-8B-Instruct")
lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, lora_config)
# ... load dataset, train ...
model.save_pretrained("./my-lora-adapter")

C. Merging the Adapter

After training, you can merge the LoRA weights into your base model (for deployment) or keep them separate (for modularity).

8.3 Using a LoRA-Adapted Model in LLamaSharp

Once you’ve trained or acquired a LoRA adapter, it must be merged with your base model. Tools like llama.cpp provide utilities for this; the merged GGUF file is then compatible with LLamaSharp.

Steps:

  1. Merge the adapter: Use merge_lora.py or equivalent from llama.cpp to combine base and LoRA weights into a new GGUF file.

  2. Load in LLamaSharp: Simply point your model path to the new GGUF.

var modelPath = @"C:\models\llama-3-8b-finetuned.gguf";
var model = LLamaModel.Load(new ModelParams(modelPath) { GpuLayerCount = 30 });
// Use as before
  1. Test and validate: Run your prompt suite to confirm that the new behavior or skill is effective and stable.

Pro Tip: Keep the base model unchanged, storing LoRA adapters as versioned files. This supports A/B testing, fast rollbacks, and multi-skill deployments.


9 Production-Ready: Deployment, Security, and MLOps

Turning a proof-of-concept into an enterprise-grade, on-premises AI system demands careful attention to deployment, security, and operational resilience. Let’s translate architectural rigor into day-to-day reliability and trust.

9.1 Architectural and Deployment Patterns

9.1.1 The API Facade: ASP.NET Core Web API

A well-designed API facade separates your AI logic from consumer applications. By wrapping inference, RAG, and supporting services behind a clean, versioned HTTP interface, you unlock flexibility and maintainability.

Key Patterns:

  • Expose a minimal, well-documented API surface for inferencing and RAG operations (/api/rag/ask, /api/llm/complete).
  • Decouple client and backend lifecycles—applications (bots, dashboards, office integrations) call a unified, stable endpoint.
  • Encapsulate business logic (preprocessing, context construction, post-processing) inside the API, not the client.

Sample Startup:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Dependency injection for model and RAG services
var app = builder.Build();
app.MapControllers();
app.Run();

This approach also facilitates robust integration with authentication, observability, and traffic management tools.

9.1.2 Containerization with Docker

Docker containers are now the de facto unit of deployment in the enterprise. They ensure reproducibility, isolation, and easy movement across dev, test, and prod environments.

Key Considerations:

  • Image Layering: Place the least frequently changed layers (base image, model weights) first. Application code changes often, so minimize rebuild times.

  • Handling Large Model Files: Use multi-stage builds, and consider mounting model files as Docker volumes at runtime if images get too large. Keep your images under 10GB if possible.

  • Sample Dockerfile:

    FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
    WORKDIR /app
    # Copy only model files (may be mounted in prod)
    COPY models/ ./models/
    # Copy published app
    COPY bin/Release/net8.0/publish/ .
    ENTRYPOINT ["dotnet", "YourApp.dll"]
  • Docker Ignore: Use .dockerignore to exclude local data and temp files.

9.1.3 Kubernetes Deployment: Scaling and Scheduling with GPUs

Kubernetes brings robust orchestration, self-healing, and horizontal scaling to AI workloads.

GPU-Enabled Deployments:

  • Use the NVIDIA device plugin for Kubernetes to expose GPUs to your pods.
  • Specify resource requests and limits for nvidia.com/gpu to ensure proper scheduling.
  • Deploy with node selectors or taints to ensure pods land on GPU nodes.

Example Pod Spec:

resources:
  limits:
    nvidia.com/gpu: 1 # Request a GPU
  • Model File Management: Store model files in a shared persistent volume or leverage container image layers.

Operational Patterns:

  • Blue-green or canary deployments for model upgrades.
  • Horizontal Pod Autoscaling based on CPU, memory, or custom metrics like request queue length.

9.2 Security in Depth

Security cannot be an afterthought, especially for on-prem LLMs handling sensitive or regulated data.

9.2.1 Preventing Prompt Injection

Prompt injection is a genuine risk—attackers can attempt to manipulate model behavior by crafting malicious queries.

Best Practices:

  • Sanitize all user input; filter or escape potentially hazardous content.
  • Limit the types of system prompts and instructions allowed in the API.
  • Use allow-lists for functions or actions that the model can trigger (if you expose function-calling).

9.2.2 Securing Model Files on Disk

Model weights are a critical asset. Treat them with the same care as private keys or database files.

  • Store models on encrypted file systems where possible.
  • Set strict file permissions; only the inference user/process should have access.
  • Do not bake secrets or private keys into the same storage path.

9.2.3 Authentication and Authorization

Your inference API should never be publicly exposed without controls.

  • Use API keys, OAuth2/JWT, or enterprise identity providers to gate access.
  • Apply least-privilege authorization—some endpoints (e.g., model reloads, admin functions) should be restricted to operators.
  • Log all access attempts and audit periodically.

9.3 Health Checks and Monitoring

Reliability comes from visibility—continuous monitoring and clear health signals enable fast incident response and proactive maintenance.

9.3.1 Exposing a /health Endpoint

Implement a lightweight /health endpoint in your API that checks:

  • Model file accessibility and successful load status.
  • GPU (or CPU) availability and status (use nvidia-smi or APIs).
  • Vector database connectivity (for RAG systems).

Sample Endpoint:

[HttpGet("/health")]
public IActionResult Health()
{
    var health = new
    {
        ModelLoaded = _model.IsLoaded,
        GpuHealthy = GpuChecker.Check(),
        VectorDbConnected = _vectorDb.Ping()
    };
    return Ok(health);
}

Integrate these checks with your deployment orchestrator (Kubernetes readiness/liveness probes).

9.3.2 Monitoring with Prometheus and Grafana

Expose key application metrics (TTFT, TPS, request counts, errors) via /metrics using libraries like prometheus-net.

  • Prometheus: Scrapes and stores time-series data.
  • Grafana: Visualizes health, usage, and performance—create dashboards for inference latency, GPU memory, vector DB queries, and error rates.

Set up alerts for abnormal spikes, memory exhaustion, or slow response times.


10 Conclusion: The Architect’s Role in the On-Prem AI Revolution

10.1 Recap of Key Takeaways

The on-prem AI movement isn’t about rejecting the cloud; it’s about reclaiming agency, privacy, and predictability.

  • Privacy and Compliance: Local LLMs keep sensitive data inside your network—no data leaves your control.
  • Cost and Control: Hardware investments are predictable; no surprise API bills. Models, not vendors, are at the center.
  • Performance: Sub-second response times and consistent throughput for interactive, mission-critical workloads.
  • Customization: Deep flexibility to adapt models, prompts, and orchestration patterns to your evolving business needs.

10.2 The Evolving .NET AI Ecosystem

.NET has rapidly matured as an AI platform. System.Numerics.Tensors brings performant, native tensor operations to the core framework. Integration with ONNX Runtime and community projects like LLamaSharp keeps .NET relevant for both inference and orchestration.

The future will likely see:

  • Deeper AI primitives: Expect even tighter integration of tensor and ML operations directly in the runtime.
  • Smarter concurrency: Better parallelization, smarter batching, and unified scheduling for CPU and GPU workloads.
  • Unified pipelines: First-class support for streaming, RAG, and hybrid cloud/on-prem models, making .NET a top-tier AI platform across sectors.

10.3 Final Thoughts: Empowering Your Organization for AI Independence

Building on-premises, open-source LLM solutions isn’t just a technical choice—it’s a strategic commitment to privacy, efficiency, and adaptability. .NET architects and principal engineers are uniquely positioned to lead this evolution, leveraging their understanding of robust enterprise systems to integrate world-class AI that serves organizational priorities.

By adopting proven patterns, monitoring what matters, and continuously iterating on both architecture and practice, you help future-proof your business. The tools and techniques outlined here are just the beginning. As open LLMs continue to improve, and as the .NET ecosystem deepens its AI capabilities, the possibilities for secure, reliable, and transformative enterprise AI will only grow.

Your architecture decisions today set the stage for tomorrow’s breakthroughs. Continue learning, experimenting, and leading—your influence shapes the on-prem AI future.

Advertisement