<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Llama-Cpp on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/llama-cpp/</link><description>Recent content in Llama-Cpp on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Wed, 27 May 2026 11:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/llama-cpp/index.xml" rel="self" type="application/rss+xml"/><item><title>Quantization for LLM inference: FP8, INT4 (GPTQ, AWQ) and GGUF — the model's accounting zoom</title><link>https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/</link><pubDate>Wed, 27 May 2026 11:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/</guid><description>&lt;blockquote>
&lt;p>This post complements &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory of LLM inference&lt;/a>, where cache quantisation is mentioned as a savings lever; here we go into the whole method, model weights and cache, and into why each format does what it does.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Quantising is a change of representation: instead of storing every model weight as a &lt;code>float16&lt;/code> or &lt;code>bfloat16&lt;/code> (2 bytes), it is stored as a short integer (1 byte INT8, half a byte INT4) with a scale factor that reconstructs a value close to the original. The price is loss of numerical precision; the reward is 2-4× less VRAM, 2-3× more throughput and, on Hopper and Blackwell, a radically lower compute cost because FP8/FP4 units execute in fewer cycles than BF16 ones. The four dominant formats in May 2026 are &lt;strong>FP8&lt;/strong> (E4M3/E5M2, datacenter), &lt;strong>INT4 GPTQ&lt;/strong> (Hessian-aware reconstruction), &lt;strong>INT4 AWQ&lt;/strong> (activation-aware) and &lt;strong>GGUF&lt;/strong> (the llama.cpp family). Each has a sweet spot: FP8 when the datacenter is Hopper/Blackwell and quality matters; GPTQ and AWQ when serving runs on Ampere/Ada and 4 bits are mandatory; GGUF when the target is edge or a consumer GPU. This post explains the minimum maths, the algorithms behind each format, what each one loses measured in perplexity and MMLU, and how it all applies on a 4090 versus an H100 cluster.&lt;/p>
&lt;h2 id="you-are-here-deploy">You are here: DEPLOY&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#qm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#qm)}&lt;/style>
&lt;defs>&lt;marker id="qm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · weight and KV cache quantization&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-jpeg-with-an-edge-detector">The analogy: JPEG with an edge detector&lt;/h2>
&lt;p>A JPEG compresses an image by reducing the precision with which pixels are stored, but it does not reduce it uniformly. Where there is flat sky, thousands of very similar pixels, it throws away detail without anyone noticing. Where there is a sharp edge, the outline of a face, it keeps the fidelity. The trick is to detect which parts are sensitive before compressing.&lt;/p>
&lt;p>Quantization of an LLM works the same way. You do not take every model weight and say &amp;ldquo;all of them in 4 bits&amp;rdquo;. Some weights are very important, projection weights that move the output a lot when they change, and others are less so. Modern techniques (GPTQ, AWQ) are basically &lt;strong>edge detectors&lt;/strong>: they identify which weights can be quantised aggressively and which need more bits or special treatment, and they apply the quantisation with that information.&lt;/p>
&lt;p>The analogy holds up in three details:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Calibration with a small dataset&lt;/strong> = the phase in which the JPEG encoder analyses the image before choosing blocks.&lt;/li>
&lt;li>&lt;strong>A block of 128 weights with a shared scale&lt;/strong> = the JPEG 8×8 block with its DCT.&lt;/li>
&lt;li>&lt;strong>Outliers preserved with more precision&lt;/strong> = the high frequencies of an edge are preserved more than the flat areas.&lt;/li>
&lt;/ul>
&lt;p>From there on, what follows is the maths and the operational detail.&lt;/p>
&lt;h2 id="the-minimum-maths-scale-and-zero-point">The minimum maths: scale and zero-point&lt;/h2>
&lt;p>Quantising a weight vector &lt;code>w ∈ ℝ^n&lt;/code> (in BF16) to INT4 means finding two things:&lt;/p>
&lt;ul>
&lt;li>A &lt;strong>scale&lt;/strong> &lt;code>s ∈ ℝ&lt;/code> (in BF16 or FP16).&lt;/li>
&lt;li>For each weight, an integer &lt;strong>code&lt;/strong> &lt;code>q ∈ {0, 1, ..., 15}&lt;/code> that fits in 4 bits.&lt;/li>
&lt;/ul>
&lt;p>And an approximate reconstruction formula:&lt;/p>
$$\hat{w}_i \approx s \cdot (q_i - z)$$
&lt;p>where &lt;code>z&lt;/code> is the &lt;strong>zero-point&lt;/strong> (the integer that defines which code represents the original zero). The zero-point exists in asymmetric INT4/INT8 so that half the range is not wasted on negative values when the weight distribution is not symmetric.&lt;/p>
&lt;p>The choice of &lt;code>s&lt;/code> and &lt;code>z&lt;/code> for a block of weights &lt;code>w_block&lt;/code>:&lt;/p>
$$s = \frac{\max(w_\text{block}) - \min(w_\text{block})}{2^{b} - 1}, \quad z = -\frac{\min(w_\text{block})}{s},$$
&lt;p>with &lt;code>b&lt;/code> = number of bits (4 in INT4). Encoding:&lt;/p>
$$q_i = \text{clip}\!\left(\text{round}\!\left(\frac{w_i}{s} + z\right),\, 0,\, 2^b - 1\right).$$
&lt;p>And decoding at inference time:&lt;/p>
$$\hat{w}_i = s \cdot (q_i - z).$$
&lt;h3 id="numerical-example">Numerical example&lt;/h3>
&lt;p>Take 8 real weights from a linear layer: &lt;code>w = [0.31, -0.12, 0.78, -0.05, 1.42, -0.91, 0.23, 0.66]&lt;/code>. We want to quantise to INT4 (16 levels).&lt;/p>
&lt;p>&lt;code>max = 1.42&lt;/code>, &lt;code>min = -0.91&lt;/code>. Range = 2.33.&lt;/p>
$$s = \frac{2.33}{15} \approx 0.1553, \quad z = -\frac{-0.91}{0.1553} \approx 5.86 \to 6.$$
&lt;p>Encoding of each weight:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;code>w_i&lt;/code>&lt;/th>
&lt;th>&lt;code>w_i/s + z&lt;/code>&lt;/th>
&lt;th>&lt;code>round&lt;/code>&lt;/th>
&lt;th>&lt;code>q_i&lt;/code>&lt;/th>
&lt;th>&lt;code>ŵ_i = s·(q-z)&lt;/code>&lt;/th>
&lt;th>error&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0.31&lt;/td>
&lt;td>8.00&lt;/td>
&lt;td>8&lt;/td>
&lt;td>8&lt;/td>
&lt;td>0.311&lt;/td>
&lt;td>+0.001&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-0.12&lt;/td>
&lt;td>5.23&lt;/td>
&lt;td>5&lt;/td>
&lt;td>5&lt;/td>
&lt;td>-0.155&lt;/td>
&lt;td>-0.035&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.78&lt;/td>
&lt;td>11.02&lt;/td>
&lt;td>11&lt;/td>
&lt;td>11&lt;/td>
&lt;td>0.776&lt;/td>
&lt;td>-0.004&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-0.05&lt;/td>
&lt;td>5.68&lt;/td>
&lt;td>6&lt;/td>
&lt;td>6&lt;/td>
&lt;td>0.000&lt;/td>
&lt;td>+0.050&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1.42&lt;/td>
&lt;td>15.14&lt;/td>
&lt;td>15&lt;/td>
&lt;td>15&lt;/td>
&lt;td>1.398&lt;/td>
&lt;td>-0.022&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-0.91&lt;/td>
&lt;td>-0.06&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>-0.932&lt;/td>
&lt;td>-0.022&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.23&lt;/td>
&lt;td>7.48&lt;/td>
&lt;td>7&lt;/td>
&lt;td>7&lt;/td>
&lt;td>0.155&lt;/td>
&lt;td>-0.075&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.66&lt;/td>
&lt;td>10.25&lt;/td>
&lt;td>10&lt;/td>
&lt;td>10&lt;/td>
&lt;td>0.621&lt;/td>
&lt;td>-0.039&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Mean squared error: &lt;code>MSE ≈ 0.0015&lt;/code>. For a single layer with millions of weights, the aggregate effect is what calibration tries to minimise.&lt;/p>
&lt;p>Storage: instead of 8 values × 2 bytes = 16 bytes (BF16), we have 8 × 4 bits = 4 bytes + 2 bytes (BF16 scale) + 0.5 byte (INT4 zero-point) ≈ 6.5 bytes. 2.5× less, yet the useful data is still recoverable with a small error.&lt;/p>
&lt;h2 id="ptq-vs-qat-when-quantisation-happens">PTQ vs QAT: when quantisation happens&lt;/h2>
&lt;p>Two different operational regimes.&lt;/p>
&lt;p>&lt;strong>Post-Training Quantization (PTQ)&lt;/strong> is applied after training, on a model already trained in BF16/FP16. It reads a small dataset (typically 128-512 examples) to calibrate the scales, runs the quantisation algorithm (GPTQ, AWQ, and so on) and produces the quantised weights. &lt;strong>Cost&lt;/strong>: minutes to a few hours. &lt;strong>Typical loss&lt;/strong>: 0.05-0.3 PPL in perplexity (~0.5-2 % in MMLU) for INT4 with modern methods.&lt;/p>
&lt;p>&lt;strong>Quantization-Aware Training (QAT)&lt;/strong> introduces the quantisation operations inside the training loop. During training the model &amp;ldquo;sees&amp;rdquo; that its weights get quantised and learns to be robust to it. &lt;strong>Cost&lt;/strong>: retraining the model (expensive), but little is needed, a short fine-tune on top of the already quantised PTQ model. &lt;strong>Typical loss&lt;/strong>: ~0 (quantisation becomes indistinguishable from the original model).&lt;/p>
&lt;p>&lt;strong>When to use which:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PTQ&lt;/strong> = default. 90 % of production cases. The model arrives in BF16, you quantise it in 1-2 hours on one GPU, you deploy it.&lt;/li>
&lt;li>&lt;strong>QAT&lt;/strong> = when PTQ loses too much and the difference matters (typical case: INT2/INT3, or sensitive models such as specific reasoning ones).&lt;/li>
&lt;/ul>
&lt;h2 id="the-dominant-formats-in-2026">The dominant formats in 2026&lt;/h2>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Dominant quantization formats">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.b1{fill:#ffe6d6}
.b2{fill:#d6eaff}
.b3{fill:#d9f5d6}
.b4{fill:#fff5b0}
.t{font:700 13px sans-serif;fill:#222}
.s{font:400 11px sans-serif;fill:#555}
.h{font:700 14px sans-serif;fill:#222}
&lt;/style>
&lt;text x="380" y="22" text-anchor="middle" class="h">Map of weight quantization formats (May 2026)&lt;/text>
&lt;rect x="20" y="40" width="170" height="300" class="bx b1"/>
&lt;text x="105" y="62" text-anchor="middle" class="t">FP8 (E4M3/E5M2)&lt;/text>
&lt;text x="30" y="84" class="s">Datacenter / Hopper-Blackwell&lt;/text>
&lt;text x="30" y="102" class="s">— H100, H200, B200&lt;/text>
&lt;text x="30" y="120" class="s">— native in vLLM&lt;/text>
&lt;text x="30" y="138" class="s">— FP8 tensor core hardware&lt;/text>
&lt;text x="30" y="160" class="t">Loss:&lt;/text>
&lt;text x="30" y="176" class="s">— PPL: +0.02-0.05&lt;/text>
&lt;text x="30" y="192" class="s">— MMLU: -0.3-0.8 pp&lt;/text>
&lt;text x="30" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="30" y="230" class="s">Model serving in a modern&lt;/text>
&lt;text x="30" y="246" class="s">datacenter. Quality almost identical&lt;/text>
&lt;text x="30" y="262" class="s">to BF16, ~2× less VRAM.&lt;/text>
&lt;text x="30" y="286" class="t">vLLM command:&lt;/text>
&lt;text x="30" y="304" class="s" font-family="monospace">--quantization=fp8&lt;/text>
&lt;text x="30" y="320" class="s" font-family="monospace">--kv-cache-dtype=fp8&lt;/text>
&lt;rect x="200" y="40" width="170" height="300" class="bx b2"/>
&lt;text x="285" y="62" text-anchor="middle" class="t">INT4 GPTQ&lt;/text>
&lt;text x="210" y="84" class="s">Hessian-aware reconstruction&lt;/text>
&lt;text x="210" y="102" class="s">— Ampere/Ada/Hopper&lt;/text>
&lt;text x="210" y="120" class="s">— vLLM, TensorRT-LLM, ExLlama&lt;/text>
&lt;text x="210" y="138" class="s">— calibration: 128 samples&lt;/text>
&lt;text x="210" y="160" class="t">Loss:&lt;/text>
&lt;text x="210" y="176" class="s">— PPL: +0.15-0.30&lt;/text>
&lt;text x="210" y="192" class="s">— MMLU: -1.5-3 pp&lt;/text>
&lt;text x="210" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="210" y="230" class="s">GPU serving without FP8 (Ampere/Ada),&lt;/text>
&lt;text x="210" y="246" class="s">mid-size models (8-70B).&lt;/text>
&lt;text x="210" y="262" class="s">~4× less VRAM.&lt;/text>
&lt;text x="210" y="286" class="t">vLLM command:&lt;/text>
&lt;text x="210" y="304" class="s" font-family="monospace">--quantization=gptq&lt;/text>
&lt;text x="210" y="320" class="s" font-family="monospace">(model *-GPTQ-Int4)&lt;/text>
&lt;rect x="380" y="40" width="170" height="300" class="bx b3"/>
&lt;text x="465" y="62" text-anchor="middle" class="t">INT4 AWQ&lt;/text>
&lt;text x="390" y="84" class="s">Activation-aware salient weights&lt;/text>
&lt;text x="390" y="102" class="s">— Ampere/Ada/Hopper&lt;/text>
&lt;text x="390" y="120" class="s">— vLLM, TensorRT-LLM&lt;/text>
&lt;text x="390" y="138" class="s">— keeps 1 % outlier channels&lt;/text>
&lt;text x="390" y="160" class="t">Loss:&lt;/text>
&lt;text x="390" y="176" class="s">— PPL: +0.10-0.25&lt;/text>
&lt;text x="390" y="192" class="s">— MMLU: -1-2 pp&lt;/text>
&lt;text x="390" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="390" y="230" class="s">Preferred alternative to GPTQ&lt;/text>
&lt;text x="390" y="246" class="s">in 2026. Better quality&lt;/text>
&lt;text x="390" y="262" class="s">preservation at similar cost.&lt;/text>
&lt;text x="390" y="286" class="t">vLLM command:&lt;/text>
&lt;text x="390" y="304" class="s" font-family="monospace">--quantization=awq_marlin&lt;/text>
&lt;text x="390" y="320" class="s" font-family="monospace">(model *-AWQ-INT4)&lt;/text>
&lt;rect x="560" y="40" width="180" height="300" class="bx b4"/>
&lt;text x="650" y="62" text-anchor="middle" class="t">GGUF (llama.cpp)&lt;/text>
&lt;text x="570" y="84" class="s">Edge / consumer / CPU-friendly&lt;/text>
&lt;text x="570" y="102" class="s">— CPU, Apple Silicon,&lt;/text>
&lt;text x="570" y="120" class="s"> consumer GPU (4090, AMD)&lt;/text>
&lt;text x="570" y="138" class="s">— sub-formats: Q4_K_M, Q5_K_M…&lt;/text>
&lt;text x="570" y="160" class="t">Loss (Q4_K_M):&lt;/text>
&lt;text x="570" y="176" class="s">— PPL: +0.20-0.40&lt;/text>
&lt;text x="570" y="192" class="s">— MMLU: -2-4 pp&lt;/text>
&lt;text x="570" y="214" class="t">Sweet spot:&lt;/text>
&lt;text x="570" y="230" class="s">Any non-CUDA deploy or one&lt;/text>
&lt;text x="570" y="246" class="s">with limited VRAM. Ollama, LMStudio.&lt;/text>
&lt;text x="570" y="262" class="s">~4× less VRAM/RAM.&lt;/text>
&lt;text x="570" y="286" class="t">Command:&lt;/text>
&lt;text x="570" y="304" class="s" font-family="monospace">ollama run llama3:8b-q4_K_M&lt;/text>
&lt;text x="570" y="320" class="s" font-family="monospace">llama.cpp --model *.gguf&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="fp8-the-hopperblackwell-datacenter-format">FP8: the Hopper/Blackwell datacenter format&lt;/h2>
&lt;p>FP8 is not &amp;ldquo;INT8 + sign&amp;rdquo;: these are two 8-bit floating point formats.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>E4M3&lt;/strong> (4 exponent bits, 3 mantissa bits): range ±448, reasonable precision around ±1.0. Typically used for &lt;strong>weights&lt;/strong> and for &lt;strong>activations in most layers&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>E5M2&lt;/strong> (5 exponent, 2 mantissa): range ±57,344, lower precision. Used for &lt;strong>gradients&lt;/strong> during training or for activations with large outliers at inference time.&lt;/li>
&lt;/ul>
&lt;p>Why FP8 left INT8 behind in the datacenter: the tensor cores on H100/H200/B200 execute FP8 operations natively with 2× the throughput of BF16 and 4× that of FP16. And because FP8 preserves logarithmic dynamics (just like FP16), matrices with values spread out in magnitude, typical of transformers, quantise with less error than with INT8.&lt;/p>
&lt;p>The loss measured in production is minimal: for a Llama 3.1 70B FP8 versus BF16, perplexity rises by ~0.03 and MMLU drops by ~0.5 points. It is the default option in any modern deployment on H100/B200.&lt;/p>
&lt;h3 id="microscaling-nvfp4-and-mxfp4">Microscaling: NVFP4 and MXFP4&lt;/h3>
&lt;p>Blackwell (B100/B200, 2025) introduces &lt;strong>NVFP4&lt;/strong> and &lt;strong>MXFP4&lt;/strong>, 4-bit formats with small-block scaling (typically 16 or 32 elements per scale, against 128 in INT4 GPTQ). The scale is FP8 instead of FP16/BF16, which reduces storage further.&lt;/p>
&lt;p>The result: 4 bits with quality close to FP8. In 2026 NVFP4 is becoming the default option for very large models (200B+) on Blackwell clusters. For 4×H100 SXM, Hopper rather than Blackwell, FP8 remains the sweet spot.&lt;/p>
&lt;h2 id="int4-gptq-vs-awq">INT4: GPTQ vs AWQ&lt;/h2>
&lt;p>The two algorithms that dominate 4-bit quantisation solve the same problem with different strategies.&lt;/p>
&lt;h3 id="gptq-frantar-et-al-2022">GPTQ (Frantar et al. 2022)&lt;/h3>
&lt;p>The idea: layer-by-layer quantisation, explicitly minimising the error at the output of each linear layer using information from the Hessian matrix (the second derivative of the loss). For each layer:&lt;/p>
&lt;ol>
&lt;li>Estimate the Hessian &lt;code>H = X^T X&lt;/code> where &lt;code>X&lt;/code> are the calibration activations.&lt;/li>
&lt;li>Quantise one weight at a time in order (typically the most sensitive one first).&lt;/li>
&lt;li>&lt;strong>Update the remaining weights&lt;/strong> to compensate for the error of the weight just quantised.&lt;/li>
&lt;/ol>
&lt;p>Step 3 is what makes GPTQ better than naive round-to-nearest: weights compensate for their neighbours&amp;rsquo; errors. The official implementation quantises a Llama 3 70B in ~3-4 hours on one H100 with 128 calibration samples.&lt;/p>
&lt;h3 id="awq-lin-et-al-2023">AWQ (Lin et al. 2023)&lt;/h3>
&lt;p>The AWQ observation: within a layer, not all channels (weight columns) are equally important. Roughly &lt;strong>1 % of the channels&lt;/strong> account for most of the impact on the activations. AWQ identifies them by measuring the mean magnitude of the activations that multiply them, and scales them before quantising so they are better preserved.&lt;/p>
&lt;p>Concretely: if a channel &lt;code>c&lt;/code> has large mean activations, AWQ multiplies that channel&amp;rsquo;s weights by a factor &lt;code>s_c&lt;/code> before quantising, and when quantising the input to the next layer it divides by &lt;code>s_c&lt;/code>. The maths cancels out, but the important weights end up with more resolution inside the INT4 range. No retraining, no Hessian, faster than GPTQ (~1-2 h for 70B on an H100).&lt;/p>
&lt;h3 id="which-one-to-choose">Which one to choose&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterion&lt;/th>
&lt;th>GPTQ&lt;/th>
&lt;th>AWQ&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Quantisation speed&lt;/td>
&lt;td>Slower&lt;/td>
&lt;td>Faster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quality preserved in INT4&lt;/td>
&lt;td>Good&lt;/td>
&lt;td>Slightly better (~0.05-0.1 PPL)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported hardware&lt;/td>
&lt;td>Broad (Ampere+)&lt;/td>
&lt;td>Broad (Ampere+)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ecosystem&lt;/td>
&lt;td>Mature, widely integrated&lt;/td>
&lt;td>More recent, gaining ground&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Default in 2026&lt;/td>
&lt;td>When GPTQ artefacts already exist&lt;/td>
&lt;td>Default for new quantisations&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The practical rule in May 2026: &lt;strong>AWQ by default&lt;/strong> for new INT4. &lt;strong>GPTQ&lt;/strong> when a GPTQ-Int4 artefact published by the community already meets your requirements.&lt;/p>
&lt;h2 id="gguf-the-llamacpp-ecosystem">GGUF: the llama.cpp ecosystem&lt;/h2>
&lt;p>GGUF is not a quantisation algorithm, it is a file format, and a whole tooling ecosystem, around the llama.cpp runtime.&lt;/p>
&lt;p>Its value: universal compatibility. The same GGUF file runs on:&lt;/p>
&lt;ul>
&lt;li>Pure CPU (Intel/AMD x86, ARM).&lt;/li>
&lt;li>Apple Silicon (M1/M2/M3/M4) with Metal acceleration.&lt;/li>
&lt;li>Consumer GPUs (RTX, AMD Radeon) with layer offload to VRAM.&lt;/li>
&lt;li>Edge devices (Jetson, ARM phones).&lt;/li>
&lt;/ul>
&lt;p>That is what llama.cpp allows and vLLM/TensorRT-LLM do not. The trade-off: lower peak throughput on datacenter GPUs than vLLM.&lt;/p>
&lt;p>The most used GGUF sub-formats in 2026:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Sub-format&lt;/th>
&lt;th>Effective bits&lt;/th>
&lt;th>Relative quality&lt;/th>
&lt;th>Typical use&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Q8_0&lt;/code>&lt;/td>
&lt;td>8.5&lt;/td>
&lt;td>Almost lossless&lt;/td>
&lt;td>Baseline validation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q6_K&lt;/code>&lt;/td>
&lt;td>6.6&lt;/td>
&lt;td>Very small loss&lt;/td>
&lt;td>High quality + savings&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q5_K_M&lt;/code>&lt;/td>
&lt;td>5.7&lt;/td>
&lt;td>Small loss&lt;/td>
&lt;td>Quality/size sweet spot&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q4_K_M&lt;/code>&lt;/td>
&lt;td>4.8&lt;/td>
&lt;td>Moderate loss&lt;/td>
&lt;td>&lt;strong>Consumer default&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q4_K_S&lt;/code>&lt;/td>
&lt;td>4.5&lt;/td>
&lt;td>Moderate-to-high loss&lt;/td>
&lt;td>When Q4_K_M does not fit&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q3_K_M&lt;/code>&lt;/td>
&lt;td>3.9&lt;/td>
&lt;td>Noticeable loss&lt;/td>
&lt;td>Very constrained hardware&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Q2_K&lt;/code>&lt;/td>
&lt;td>3.3&lt;/td>
&lt;td>Large loss&lt;/td>
&lt;td>Last resort&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>_K_M&lt;/code> suffix indicates the degree of mixing: inside the file, certain layers (typically &lt;code>attention.wv&lt;/code>, &lt;code>feed_forward.w2&lt;/code>) are stored with more bits than others. It is the equivalent of the JPEG &amp;ldquo;edge detector&amp;rdquo; applied layer by layer with a pre-established heuristic.&lt;/p>
&lt;h2 id="kv-cache-quantization">KV cache quantization&lt;/h2>
&lt;p>Quantising the model weights is half the problem. The &lt;strong>KV cache&lt;/strong>, covered in detail in &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory&lt;/a>, typically consumes 20-50 % of VRAM in production with concurrency. Quantising the cache is a lever too:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>--kv-cache-dtype=auto&lt;/code>&lt;/strong> (BF16/FP16, the default). 2 bytes per dimension × num_heads × head_dim × 2 (K and V).&lt;/li>
&lt;li>&lt;strong>&lt;code>--kv-cache-dtype=fp8&lt;/code>&lt;/strong> (E4M3 or E5M2 depending on hardware). 1 byte. Halves the cache with a typical loss of &amp;lt; 0.5 % on quality benchmarks.&lt;/li>
&lt;li>&lt;strong>&lt;code>--kv-cache-dtype=int4&lt;/code>&lt;/strong> (with blocks of 128). 0.5 bytes plus scale overhead. Divides the cache by ~3.5. Measurable loss (1-2 %) but acceptable with long contexts.&lt;/li>
&lt;/ul>
&lt;p>KV cache quantisation is &lt;strong>orthogonal&lt;/strong> to weight quantisation: you can have BF16 weights and an FP8 cache, or INT4 weights and an FP8 cache, and so on. The dominant combination in 2026 on H100: &lt;strong>FP8 weights + FP8 cache&lt;/strong>, which is almost indistinguishable from BF16 in quality and doubles concurrency capacity.&lt;/p>
&lt;h2 id="measured-quality-loss-llama-31-70b-instruct-reference">Measured quality loss (Llama 3.1 70B Instruct, reference)&lt;/h2>
&lt;p>A representative table for Llama 3.1 70B Instruct with a WikiText-2 calibration dataset (128 samples). Figures aggregated from public sources; they may vary by ±0.05 PPL and ±0.5 MMLU depending on implementation and seed.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Format&lt;/th>
&lt;th>Model VRAM&lt;/th>
&lt;th>Perplexity (WikiText-2)&lt;/th>
&lt;th>MMLU (5-shot)&lt;/th>
&lt;th>Relative speed (H100)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>BF16 (baseline)&lt;/td>
&lt;td>140 GB&lt;/td>
&lt;td>4.85&lt;/td>
&lt;td>82.1&lt;/td>
&lt;td>1.00×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FP8 (E4M3)&lt;/td>
&lt;td>70 GB&lt;/td>
&lt;td>4.87 (+0.02)&lt;/td>
&lt;td>81.6 (-0.5)&lt;/td>
&lt;td>1.85×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT8 SmoothQuant&lt;/td>
&lt;td>70 GB&lt;/td>
&lt;td>4.92 (+0.07)&lt;/td>
&lt;td>81.0 (-1.1)&lt;/td>
&lt;td>1.65×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 AWQ&lt;/td>
&lt;td>35 GB&lt;/td>
&lt;td>4.99 (+0.14)&lt;/td>
&lt;td>80.4 (-1.7)&lt;/td>
&lt;td>2.50×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INT4 GPTQ&lt;/td>
&lt;td>35 GB&lt;/td>
&lt;td>5.05 (+0.20)&lt;/td>
&lt;td>80.0 (-2.1)&lt;/td>
&lt;td>2.40×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GGUF Q5_K_M&lt;/td>
&lt;td>49 GB&lt;/td>
&lt;td>4.94 (+0.09)&lt;/td>
&lt;td>81.1 (-1.0)&lt;/td>
&lt;td>n/a (llama.cpp)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GGUF Q4_K_M&lt;/td>
&lt;td>42 GB&lt;/td>
&lt;td>5.08 (+0.23)&lt;/td>
&lt;td>79.8 (-2.3)&lt;/td>
&lt;td>n/a (llama.cpp)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GGUF Q3_K_M&lt;/td>
&lt;td>33 GB&lt;/td>
&lt;td>5.45 (+0.60)&lt;/td>
&lt;td>77.5 (-4.6)&lt;/td>
&lt;td>n/a (llama.cpp)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Three lessons worth keeping:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>FP8 is almost free in quality terms&lt;/strong>. If your hardware supports it, there is no debate.&lt;/li>
&lt;li>&lt;strong>INT4 AWQ is noticeably better than INT4 GPTQ&lt;/strong> in preserved quality, at comparable speed.&lt;/li>
&lt;li>&lt;strong>Q3 is already in measurable-loss territory&lt;/strong>; Q2 should no longer be used except for experiments or extreme demos.&lt;/li>
&lt;/ol>
&lt;h2 id="implications-for-on-premise-hardware">Implications for on-premise hardware&lt;/h2>
&lt;h3 id="on-an-rtx-4090-24-gb-ada-lovelace-no-native-fp8">On an RTX 4090 (24 GB, Ada Lovelace, no native FP8)&lt;/h3>
&lt;p>Llama 3.1 8B Instruct fits comfortably in BF16 (16 GB), but that leaves little headroom for a KV cache under concurrency. The usual sweet spot:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3.1 8B AWQ-INT4&lt;/strong>: ~5 GB of weights, 19 GB free for the KV cache → 4-8 concurrent sessions with a moderate context.&lt;/li>
&lt;li>&lt;strong>Llama 3 70B GGUF Q4_K_M&lt;/strong>: ~42 GB. &lt;strong>It does not fit on a whole 4090&lt;/strong>; it requires CPU offload with llama.cpp (slow decode but workable for a single user).&lt;/li>
&lt;li>&lt;strong>Llama 3 70B AWQ-INT4 with TP=2 (two 4090s)&lt;/strong>: ~17 GB per GPU → it fits and leaves headroom.&lt;/li>
&lt;/ul>
&lt;p>The 4090 &lt;strong>does not support native FP8&lt;/strong> (Ada Lovelace has the instruction but not Hopper&amp;rsquo;s accelerated throughput). In practice, FP8 on a 4090 works but without the speed gain: the sensible choice is INT4 AWQ.&lt;/p>
&lt;h3 id="on-a-generic-4h100-sxm-cluster-320-gb-nvlink-native-fp8">On a generic 4×H100 SXM cluster (320 GB, NVLink, native FP8)&lt;/h3>
&lt;p>Here FP8 shines:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3.1 70B FP8&lt;/strong> with TP=2: ~35 GB/GPU. Comfortable, leaving a huge amount of room for the KV cache → dozens of concurrent sessions.&lt;/li>
&lt;li>&lt;strong>Llama 3.1 405B FP8&lt;/strong> with TP=4: ~200 GB/GPU. It just fits, with prefill+decode in the same pool.&lt;/li>
&lt;li>&lt;strong>Llama 3.1 405B INT4 AWQ&lt;/strong> with TP=2: ~100 GB/GPU. It allows serving the large model without saturating the cluster; there is headroom left for the cache and for serving another model at the same time.&lt;/li>
&lt;/ul>
&lt;p>The rule of thumb on an H100 cluster in 2026: &lt;strong>FP8 if quality matters and the model fits; INT4 AWQ if the model does not fit in FP8 or if you want more concurrency at the cost of 1-2 MMLU points&lt;/strong>.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Speculative decoding&lt;/strong>: the other big acceleration lever in inference. Orthogonal to quantization, it multiplies the speedup.&lt;/li>
&lt;li>&lt;strong>MoE quantization&lt;/strong>: Mixture-of-Experts models (Mixtral, DeepSeek V3, Qwen3-235B-A22B) have different quantisation patterns, experts are not quantised uniformly and there is dynamic routing.&lt;/li>
&lt;li>&lt;strong>Calibration dataset matters&lt;/strong>: how to choose the 128-512 calibration samples. The common mistake of grabbing a random dataset off the internet, and how to avoid it.&lt;/li>
&lt;li>&lt;strong>Multimodal quantization&lt;/strong>: vision-language models have heterogeneous layers (a CNN vision encoder, a transformer language model) that need separate treatment.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory that holds up LLM inference&lt;/a> — the cache is also quantised with the same formats (FP8, INT4); this post goes into the detail of how, and with what loss.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving: prefill and decode in specialised pods&lt;/a> — the two pools can be quantised &lt;strong>asymmetrically&lt;/strong>: prefill with less compression (quality), decode with more compression (throughput).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — QLoRA uses NF4, a specific INT4 variant for the base model during training. It is the cousin of the inference formats described here.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/modern-alignment-dpo-kto-orpo-simpo/">Modern alignment: DPO, KTO, ORPO, SimPO&lt;/a> — the reference model (&lt;code>π_ref&lt;/code>) in DPO can be quantised to FP8 to free up VRAM. The quality of the reference matters less than that of the trained model.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map where Deploy is stage 4.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who anticipates what the boss is going to say&lt;/a> — a lever orthogonal to and multiplicative with quantization. The draft is usually left in BF16 even when the target is in FP8/INT4, because quantising it degrades α and kills the speedup.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/flashattention-librarian-io-awareness-async-blackwell-asymmetry/">FlashAttention v1/v2/v3/v4: the librarian who never clears the desk&lt;/a> — FA3 does FP8 attention with block quantization and incoherent processing to preserve precision; FA4 combines that with Blackwell NVFP4 weights. The two quantisation layers (weights and attention) are applied at different moments in the kernel.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/moe-inference-fundamentals-expert-parallel-deepseek-economics/">MoE inference: the call centre with 256 specialists&lt;/a> — DeepSeek-V3 671B FP8 (~685 GB) only fits on modest clusters thanks to aggressive quantisation of the routed experts; NVFP4 on expert weights + FP8 attention is the peak setup on NVL72.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">Multi-LoRA serving: one translator with a thousand glossaries&lt;/a> — the canonical stack is an FP8/INT4 base plus BF16 adapters. Quantising the base does not just free memory, it multiplies the economics: the same cluster goes from serving dozens to hundreds of concurrent adapters.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">Optimising decode in vLLM&lt;/a> — the combination of AWQ (INT4 weights) + FP8 KV cache + speculative decoding is the reference config for squeezing an RTX 4090; the concrete numbers are laid out there.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">Optimising prefill in vLLM&lt;/a> — &lt;code>--kv-cache-dtype fp8&lt;/code> and &lt;code>--calculate-kv-scales&lt;/code> are the parameters that enable KV cache quantisation in vLLM, with the precision trade-off measured on long contexts.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-pruning-cutting-without-amputating/">Pruning LLM models&lt;/a> — an orthogonal lever: quantising reduces the precision of the weights pruning has kept; combined, 50% Wanda sparsity + AWQ INT4 shrinks the model to ~1/8 of its original size.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/knowledge-distillation-teaching-small-model-think-like-big-one/">Knowledge distillation&lt;/a> — the previous step in the pipeline: you distil first (a new, smaller model), you quantise afterwards (the same model, more efficient); the two techniques attack different dimensions.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — quantization is the first sizing lever when the BF16 arithmetic does not work out; the sensitivity table there quantifies the savings in VRAM and in TPOT.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/test-time-quantization-without-calibration-dataset/">Test-time quantization: quantising on the fly&lt;/a> — the alternative with no calibration dataset: it derives activation-aware scales at inference time, at the cost of runtime overhead that weighs more heavily on small models.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/aggressive-quantisation-4-bit-to-ternary/">Aggressive quantisation: from 4-bit to ternary&lt;/a> — what happens below 4-bit, where naive PTQ collapses and co-design is needed: codebooks (AQLM, QuIP#, QTIP), QAT and native ternary (BitNet b1.58).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">Serving engines compared (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — how each engine supports the quantization formats described here (FP8, AWQ, GPTQ, GGUF) and what difference in goodput it makes to pick the one that implements them best.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Frantar, E., Ashkboos, S., Hoefler, T., Alistarh, D. &lt;em>GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers&lt;/em> (ICLR 2023).&lt;/li>
&lt;li>Lin, J., Tang, J., Tang, H., Yang, S., Dang, X., Han, S. &lt;em>AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration&lt;/em> (MLSys 2024).&lt;/li>
&lt;li>Dettmers, T., Pagnoni, A., Holtzman, A., Zettlemoyer, L. &lt;em>QLoRA: Efficient Finetuning of Quantized LLMs&lt;/em> (NeurIPS 2023). Introduces NF4 and double quantization.&lt;/li>
&lt;li>Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J., Han, S. &lt;em>SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models&lt;/em> (ICML 2023).&lt;/li>
&lt;li>NVIDIA. &lt;em>FP8 Formats for Deep Learning&lt;/em> — E4M3/E5M2 white paper: &lt;a href="https://arxiv.org/abs/2209.05433">https://arxiv.org/abs/2209.05433&lt;/a>.&lt;/li>
&lt;li>Rouhani, B. et al. &lt;em>Microscaling Data Formats for Deep Learning&lt;/em> — MXFP4/MXFP8: &lt;a href="https://arxiv.org/abs/2310.10537">https://arxiv.org/abs/2310.10537&lt;/a>.&lt;/li>
&lt;li>llama.cpp GGUF spec: &lt;a href="https://github.com/ggerganov/llama.cpp/blob/master/docs/gguf.md">https://github.com/ggerganov/llama.cpp/blob/master/docs/gguf.md&lt;/a>.&lt;/li>
&lt;li>vLLM quantization docs: &lt;a href="https://docs.vllm.ai/en/latest/quantization/">https://docs.vllm.ai/en/latest/quantization/&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>eBPF in local inference and statistical drift detection: closing the LLM observability loop in 2026</title><link>https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/</link><pubDate>Wed, 20 May 2026 09:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Tracing, evals, guardrails, MCP observability: the layers we have already covered see what is happening right now. What they do not see is what changes silently: the agent that answered well last week and this week, without anyone touching anything, answers slightly worse. What they also do not see is the fine mechanics of local inference: why a llama.cpp on an edge device takes 200 ms when it should take 100, which specific runtime function is the bottleneck. This post closes the two series of the week with the two layers that were missing: eBPF applied to local inference (uprobes on &lt;code>llama.cpp&lt;/code>, &lt;code>vLLM&lt;/code>, &lt;code>libcudart.so&lt;/code>, integrated hardware perf counters, with &amp;lt;4% overhead, formalised in the &lt;a href="https://arxiv.org/abs/2601.20755">ProfInfer 2026&lt;/a> paper, which is to local inference what Hubble is to the network) and statistical analysis of agent flows to detect drift before your user notices it (KS, PSI, MMD, embedding-space clustering, with &lt;a href="https://www.evidentlyai.com/">Evidently AI&lt;/a>, &lt;a href="https://www.nannyml.com/">NannyML&lt;/a> and &lt;a href="https://whylabs.ai/">WhyLabs&lt;/a> as the dominant tools). The three types of LLM drift in 2026, prompt drift, model drift and eval-score drift, demand different tests. The complete stack (tracing, evals, guardrails, MCP observability, eBPF observability, drift detection) forms the loop that any serious agentic system needs in order to operate with a real SLA rather than with hope.&lt;/p>
&lt;blockquote>
&lt;p>This post closes two series: the post-tracing series (&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability&lt;/a>) and the eBPF series (&lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight&lt;/a>). It brings the two threads together: eBPF applied to the local inference engine plus statistical analysis of the flows that every layer produces.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-analogy-the-agents-cardiogram">The analogy: the agent&amp;rsquo;s cardiogram&lt;/h2>
&lt;p>A doctor who only looks at acute symptoms, the patient arrives with a high fever and you have to act, is practising reactive medicine. To practise preventive medicine, they need time series: blood pressure every year, cholesterol every six months, an ECG when there is a suspicion. It is not &amp;ldquo;right now&amp;rdquo; information, it is information about how something that should be stable is evolving. When a time series deviates from its baseline, you investigate before it turns into a high fever.&lt;/p>
&lt;p>The LLM observability layers we have seen so far are reactive medicine: tracing tells you what happened in a specific conversation; evals tell you whether that conversation was good; guardrails tell you whether there was a specific threat; MCP observability tells you which tools were invoked and how. They all look at events, not trends.&lt;/p>
&lt;p>Drift detection is preventive medicine. It looks at time series, of prompt embeddings, of evaluation scores, of distributions of generated tokens, and fires alerts when something drifts away from its normality. It does not tell you &amp;ldquo;this answer is bad&amp;rdquo;; it tells you &amp;ldquo;the distribution of prompts over the last 6 hours does not look like the distribution of the last month&amp;rdquo;. There you decide whether to investigate.&lt;/p>
&lt;p>And the other half of the post, eBPF in local inference, is the equivalent of the MRI scanner: once you know there is a problem, it lets you see the inside of the model at a resolution no external wrapper gives you. To see which specific runtime function is slow, which CUDA kernel is the bottleneck, how tokens move through the internal buffers before going out to the client.&lt;/p>
&lt;p>The two together close the loop: the time series detect that something is wrong, the scanner locates where.&lt;/p>
&lt;h2 id="part-1--ebpf-applied-to-local-inference">Part 1 — eBPF applied to local inference&lt;/h2>
&lt;h3 id="why-local-inference-changes-the-game">Why local inference changes the game&lt;/h3>
&lt;p>When the LLM runs locally, vLLM on a Kubernetes node, llama.cpp on an edge device, Ollama on a workstation, MLX on macOS, and not behind an external API, observability changes shape:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You control the binary&lt;/strong>: you can attach hooks that would otherwise be impossible.&lt;/li>
&lt;li>&lt;strong>The internal buffers exist in accessible RAM&lt;/strong>: the output-token stream, the logits, the KV caches, the scheduler structures are right there, at addresses a uprobe can read.&lt;/li>
&lt;li>&lt;strong>There is no cable to sniff&lt;/strong>: AgentSight&amp;rsquo;s analogy with SSL hooks does not apply because there is no TLS; the model answers you with an in-process function return, not with an HTTPS response.&lt;/li>
&lt;li>&lt;strong>The distance between kernel and model is minimal&lt;/strong>: the CUDA kernels running attention are one syscall deep; eBPF can observe both sides of that boundary with the same tracer.&lt;/li>
&lt;/ul>
&lt;p>This opens up a class of observability that is structurally impossible with LLM-as-a-service (the Anthropic, OpenAI or Vertex APIs). For apps serving inference on-premise or on-edge, an inference cluster, a mobile device, an RTX 4090 server in the rack, it is a new layer.&lt;/p>
&lt;h3 id="profinfer-the-paper-that-formalises-the-pattern">ProfInfer: the paper that formalises the pattern&lt;/h3>
&lt;p>&lt;a href="https://arxiv.org/abs/2601.20755">ProfInfer (arxiv 2601.20755, 2026)&lt;/a> is the reference academic piece that systematises what the ecosystem had been doing ad-hoc. The paper&amp;rsquo;s subtitle says it all: &lt;em>An eBPF-based Fine-Grained LLM Inference Profiler&lt;/em>.&lt;/p>
&lt;p>What it proposes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Attaching uprobes dynamically&lt;/strong> to runtime functions of engines such as &lt;code>llama.cpp&lt;/code> (and by extension vLLM, Ollama). No recompiling, no modifying the source code. It is like &lt;code>bpftrace&lt;/code> for LLM inference.&lt;/li>
&lt;li>&lt;strong>Combining runtime events with hardware performance counters&lt;/strong>. A uprobe tells you when &lt;code>llama_decode&lt;/code> runs; a hardware counter tells you how many floating-point instructions executed while it was inside. The correlation between the two is what gives the fine resolution.&lt;/li>
&lt;li>&lt;strong>&amp;lt;4% measured overhead&lt;/strong> under real workloads. That is a production-grade cost.&lt;/li>
&lt;li>&lt;strong>Visualisations&lt;/strong> in three views: operators (which tensor operations ran), graphs (how they relate) and timelines (when).&lt;/li>
&lt;/ul>
&lt;p>The paper focuses particularly on models on mobile platforms (Llama served on a Pixel or an iPhone), where classic observability with Prometheus and exported metrics barely exists. But the pattern applies to any local inference.&lt;/p>
&lt;h3 id="where-to-hook-the-map-by-engine">Where to hook: the map by engine&lt;/h3>
&lt;p>Now to the detail of the hooks. The target functions vary by engine:&lt;/p>
&lt;h4 id="llamacpp">llama.cpp&lt;/h4>
&lt;p>&lt;code>llama.cpp&lt;/code> is pure C++, with symbols visible in the binary. The typical hooks:&lt;/p>
&lt;ul>
&lt;li>&lt;code>llama_decode&lt;/code>: the function that runs one inference pass (processes the current batch). Spans for latency per iteration, tokens processed.&lt;/li>
&lt;li>&lt;code>llama_token_to_piece&lt;/code>: converts a token ID to text. A hook here captures the stream of generated tokens before returning to the caller. It is the local equivalent of the SSL uprobes: you see the model&amp;rsquo;s output before it even reaches the consumer.&lt;/li>
&lt;li>&lt;code>llama_get_logits&lt;/code>: reads the logits of the last decode. If you want to record the model&amp;rsquo;s probabilities (not just the chosen token), here.&lt;/li>
&lt;li>&lt;code>ggml_compute_forward_*&lt;/code> (several functions): the operation kernels (matmul, attention, layernorm). Hooks for per-operation profiling.&lt;/li>
&lt;li>&lt;code>ggml_backend_*&lt;/code>: the backend functions (CPU, Metal, CUDA, ROCm). Hooks here break down the cost per device.&lt;/li>
&lt;/ul>
&lt;p>An example with &lt;code>bpftrace&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Latency and count of llama_decode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">bpftrace -e &lt;span class="s1">&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">uprobe:/path/to/llama-server:llama_decode {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> @start[tid] = nsecs;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">uretprobe:/path/to/llama-server:llama_decode /@start[tid]/ {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> @decode_lat = hist((nsecs - @start[tid]) / 1000);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> delete(@start[tid]);
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Output: a histogram of decode latencies in microseconds. Zero modification to the binary.&lt;/p>
&lt;h4 id="vllm">vLLM&lt;/h4>
&lt;p>vLLM is mostly Python. The C/CUDA symbols live in its native extensions (&lt;code>vllm._C&lt;/code>, &lt;code>vllm._moe_C&lt;/code>). The typical hooks:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>uprobes on &lt;code>vllm._C.*&lt;/code>&lt;/strong> for custom operators (the paged attention kernel, the sampling kernel).&lt;/li>
&lt;li>&lt;strong>uprobes on &lt;code>libcudart.so&lt;/code> and &lt;code>libcuda.so&lt;/code>&lt;/strong> to capture &lt;code>cudaMalloc&lt;/code>, &lt;code>cudaLaunchKernel&lt;/code>, &lt;code>cudaMemcpy&lt;/code>. This maps the cost of host↔device transfers and kernel launches.&lt;/li>
&lt;li>&lt;strong>Python tracepoints with &lt;code>bpftrace&lt;/code> over &lt;code>usdt&lt;/code> points&lt;/strong>: vLLM does not expose native static tracepoints, but they can be placed with USDT (&lt;code>dtrace&lt;/code> style) at strategic points in the scheduler.&lt;/li>
&lt;/ul>
&lt;p>vLLM also exposes native Prometheus metrics (&lt;code>vllm:num_requests_running&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>, and so on). The added value of the eBPF approach is going down from the scheduler&amp;rsquo;s metrics to the individual functions: when a request is slow, seeing whether it was prefill, decode, scheduler overhead, transfer or synchronisation.&lt;/p>
&lt;h4 id="cuda-in-general">CUDA in general&lt;/h4>
&lt;p>Independently of the engine, uprobes on &lt;code>libcudart.so&lt;/code> capture all the CUDA activity of the process:&lt;/p>
&lt;ul>
&lt;li>&lt;code>cudaMalloc(size)&lt;/code> → tracking of device memory allocations.&lt;/li>
&lt;li>&lt;code>cudaLaunchKernel(func, ...)&lt;/code> → spans for each kernel launch.&lt;/li>
&lt;li>&lt;code>cudaMemcpyAsync(dst, src, size, kind)&lt;/code> → host↔device transfers.&lt;/li>
&lt;li>&lt;code>cudaStreamSynchronize(stream)&lt;/code> → synchronisation points (where the host waits for the device).&lt;/li>
&lt;/ul>
&lt;p>This gives you a complete timeline of CUDA activity without needing NVIDIA Nsight Systems (which is excellent but heavy and oriented towards development, not continuous production).&lt;/p>
&lt;h3 id="hardware-counters-the-other-half">Hardware counters: the other half&lt;/h3>
&lt;p>eBPF can read performance counters from the CPU/GPU&amp;rsquo;s PMU (Performance Monitoring Unit). That includes instructions executed, cache misses, branch mispredictions and, on GPUs with support, FLOPS, SM occupancy, HBM bandwidth.&lt;/p>
&lt;p>Combining:&lt;/p>
&lt;ul>
&lt;li>uprobe: &amp;ldquo;&lt;code>llama_decode&lt;/code> ran from T1 to T2 with tokens=4&amp;rdquo;.&lt;/li>
&lt;li>perf counter: &amp;ldquo;during that window, L2 cache misses = 15,000, instructions = 2.3 million&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>This lets you answer: why is it slow? Is it memory-bound (many cache misses), compute-bound (all the instructions in the FPU), bandwidth-bound (a lot of data movement)? State of the art for professional profiling.&lt;/p>
&lt;h3 id="comparison-with-agentsight">Comparison with AgentSight&lt;/h3>
&lt;p>There are two eBPF products for LLMs today with different focuses:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight&lt;/a>&lt;/strong> (covered in the eBPF series): observes agents that call external APIs. It hooks SSL to see the plaintext of HTTPS calls to the remote LLM, plus stdio for local MCP servers. The client view.&lt;/li>
&lt;li>&lt;strong>ProfInfer / the eBPF-on-local-inference pattern&lt;/strong>: observes the engine that runs the model locally. It hooks the engine&amp;rsquo;s internal functions (llama.cpp, vLLM) and the CUDA layer. The server (internal) view.&lt;/li>
&lt;/ul>
&lt;p>They are complementary. If your agent uses the Claude API plus your own local vLLM with Llama 3 for specific tasks, AgentSight sees the first, eBPF/ProfInfer sees the second. If everything is local, clearly the domain of the second. If everything is an external API, of the first.&lt;/p>
&lt;h3 id="use-cases-for-ebpf-in-local-inference">Use cases for eBPF in local inference&lt;/h3>
&lt;p>Three cases where it is the right tool:&lt;/p>
&lt;p>&lt;strong>Fine-grained profiling for optimisation&lt;/strong>: your vLLM takes 50ms more per token than expected. With eBPF plus hardware counters you pin down which specific kernel it is. Previously this required Nsight Systems in a development session; now it is continuous in production.&lt;/p>
&lt;p>&lt;strong>Token-level observability without modifying the engine&lt;/strong>: capturing the stream of generated tokens before returning them to the client. Useful for auditing, for drift detection over the outputs, for local tracing without going through wrapper instrumentation.&lt;/p>
&lt;p>&lt;strong>Detecting specific degradation&lt;/strong>: a new version of vLLM introduces a subtle regression in paged attention. With perf counter baselines, you detect the change even if the external metric (tokens/sec) looks the same.&lt;/p>
&lt;h2 id="part-2--statistical-analysis-of-flows-detecting-drift">Part 2 — Statistical analysis of flows: detecting drift&lt;/h2>
&lt;p>Now to the other side of the problem: the time series.&lt;/p>
&lt;h3 id="why-tracing-evals-and-guardrails-do-not-detect-drift">Why tracing, evals and guardrails do not detect drift&lt;/h3>
&lt;p>The layers we have already seen operate on individual events:&lt;/p>
&lt;ul>
&lt;li>Tracing: a trace of one conversation.&lt;/li>
&lt;li>Evals: a score for one answer.&lt;/li>
&lt;li>Guardrails: a verdict on one prompt or answer.&lt;/li>
&lt;li>MCP observability: spans of one tool invocation.&lt;/li>
&lt;/ul>
&lt;p>Each one answers a point-in-time question (&amp;ldquo;is this all right?&amp;rdquo;). None of them answers the question of evolution (&amp;ldquo;is something changing over time?&amp;rdquo;).&lt;/p>
&lt;p>The operational problem: drift is invisible in individual events. If the mean eval score drops from 0.92 to 0.85 over three weeks, no individual evaluation will raise an alarm, since they all continue to be &amp;ldquo;reasonable&amp;rdquo;. What changes is the distribution. And that only shows up when you look at many evaluations aggregated over time.&lt;/p>
&lt;h3 id="the-three-types-of-llm-drift-in-2026">The three types of LLM drift in 2026&lt;/h3>
&lt;p>&lt;a href="https://futureagi.com/blog/what-is-llm-drift-2026">FutureAGI&lt;/a> consolidates them like this, and the industry is converging on this vocabulary:&lt;/p>
&lt;p>&lt;strong>1. Prompt drift&lt;/strong>: someone updates the system prompt and the side effects break cases that used to work. Almost always intentional, but with unanticipated consequences. Detection: comparing response distributions before and after the change, monitoring eval scores per prompt version (linked in Langfuse, see the &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight&lt;/a> post where we covered prompt management).&lt;/p>
&lt;p>&lt;strong>2. Model drift&lt;/strong>: the provider (OpenAI, Anthropic) updates the model without warning. The same prompt produces answers with a slightly different tone, similar but different quality, or degradation on some subset. Detection: comparing today&amp;rsquo;s response embeddings with the baseline; monitoring rubric scores; alerting if intra-model variance grows.&lt;/p>
&lt;p>&lt;strong>3. Eval-score drift&lt;/strong>: the rolling mean of your eval metrics (faithfulness, answer relevancy, custom rubrics) trends downwards. The root cause can be any of the above or a change in the user mix. Detection: alerts on the trends of the eval series.&lt;/p>
&lt;p>To these three you can add a fourth, subtler one:&lt;/p>
&lt;p>&lt;strong>4. Persona drift / user mix shift&lt;/strong>: the population of users using the system changes. It is not that the model or the prompt got worse; it is that the new users ask different questions and the system, while still just as good at what it was good at, fails on the new material. Detection: embedding clustering of prompts, monitoring the appearance of new clusters or the growth of a minority one.&lt;/p>
&lt;h3 id="the-key-technical-concept-embedding-space-shift">The key technical concept: embedding-space shift&lt;/h3>
&lt;p>&lt;a href="https://stackpulsar.com/blog/llm-model-drift-detection/">Stack Pulsar&lt;/a> puts it plainly: in LLMs, drift is best measured in embedding space. Classic distances in token space do not capture fine semantics; in embedding space they do.&lt;/p>
&lt;p>The canonical pipeline:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Establish a baseline&lt;/strong>: during a stable period (say the first two weeks after a release), capture a large sample of prompt and response embeddings.&lt;/li>
&lt;li>&lt;strong>Continuous monitoring&lt;/strong>: every hour or every day, capture a new sample of production traffic.&lt;/li>
&lt;li>&lt;strong>Compare distributions&lt;/strong>: apply a statistical test that compares the current distribution with the baseline in embedding space.&lt;/li>
&lt;li>&lt;strong>Alert&lt;/strong>: if the divergence exceeds a threshold, fire an alert and an investigation workflow.&lt;/li>
&lt;/ol>
&lt;p>As a bonus, monitor clusters: if your baseline has 5 clusters of prompts (technical questions, general support, sales, and so on) and suddenly a sixth cluster appears that was not there, the most likely explanation is that a new user segment has arrived.&lt;/p>
&lt;h3 id="statistical-tests-ks-psi-mmd">Statistical tests: KS, PSI, MMD&lt;/h3>
&lt;p>Three tests that any drift system uses, each with its place:&lt;/p>
&lt;p>&lt;strong>Kolmogorov-Smirnov (KS)&lt;/strong>: non-parametric. It computes the maximum distance between two empirical CDFs. It returns a statistic and a p-value. Advantage: very sensitive to subtle changes, especially in the tails. Disadvantage: with large datasets it is &amp;ldquo;too sensitive&amp;rdquo;, firing alarms for changes that are real but clinically irrelevant.&lt;/p>
&lt;p>&lt;strong>Population Stability Index (PSI)&lt;/strong>: you bin the reference distribution and the current one, then sum &lt;code>(p_actual - p_ref) × log(p_actual / p_ref)&lt;/code> over the bins. The canonical interpretation: PSI &amp;lt; 0.1 stable, 0.1-0.25 mild drift, &amp;gt; 0.25 significant drift. Advantage: interpretable, threshold-based, with a tradition of use in credit scoring (Capital One, Goldman Sachs). Disadvantage: less sensitive than KS, so it misses drift in the tails.&lt;/p>
&lt;p>&lt;strong>Maximum Mean Discrepancy (MMD)&lt;/strong>: measures the divergence between two distributions by embedding each one in a Hilbert space via a kernel. It works for complex multivariate distributions (high-dimensional embeddings). Advantage: the only one that scales reasonably to embeddings of 768/1024/4096 dimensions. Disadvantage: harder to interpret.&lt;/p>
&lt;p>The recommended practice in 2026:&lt;/p>
&lt;ul>
&lt;li>PSI for simple features (prompt length, tokens, number of tools invoked).&lt;/li>
&lt;li>KS for continuous features where you want high sensitivity.&lt;/li>
&lt;li>MMD for embeddings (high-dimensional spaces).&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://www.evidentlyai.com/blog/data-drift-detection-large-datasets">Evidently&amp;rsquo;s analysis&lt;/a> on real datasets showed that KS detects drift 6+ hours earlier than PSI in some incidents. The operational consequence: use KS for early warning, PSI for confirmation with an interpretable threshold.&lt;/p>
&lt;h3 id="tools-in-2026">Tools in 2026&lt;/h3>
&lt;p>Three products dominate the field:&lt;/p>
&lt;h4 id="evidently-ai">Evidently AI&lt;/h4>
&lt;p>&lt;a href="https://github.com/evidentlyai/evidently">Evidently&lt;/a> is open-source (Apache 2.0), Python-first. Its value:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>HTML drift reports&lt;/strong>: you generate a report comparing two datasets (reference vs current) and you get an HTML file with all the statistical tests, visualisations and conclusions. No server, no infrastructure; one shareable file.&lt;/li>
&lt;li>&lt;strong>Native LLM support&lt;/strong>: on top of tabular, it supports text. It computes embeddings and applies the appropriate tests.&lt;/li>
&lt;li>&lt;strong>100+ metrics&lt;/strong> in the suite. It covers everything from a single framework.&lt;/li>
&lt;li>&lt;strong>Integration with MLflow and kube&lt;/strong>: CI workflows with reports on every release.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">evidently&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Report&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">evidently.metrics&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">DataDriftPreset&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ref&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_baseline_dataset&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># prompts from last week&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cur&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_current_dataset&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># prompts from the last hour&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">report&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Report&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">metrics&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">DataDriftPreset&lt;/span>&lt;span class="p">()])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">reference_data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ref&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">current_data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">cur&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">save_html&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;drift_report.html&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When this functionality detects drift, it also tells you which column and which test fired.&lt;/p>
&lt;h4 id="nannyml">NannyML&lt;/h4>
&lt;p>&lt;a href="https://www.nannyml.com/">NannyML&lt;/a> has a different focus: estimating model performance when you have no ground truth. The techniques:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CBPE (Confidence-Based Performance Estimation)&lt;/strong>: estimates accuracy using the model&amp;rsquo;s confidence in its predictions.&lt;/li>
&lt;li>&lt;strong>DLE (Direct Loss Estimation)&lt;/strong>: estimates the loss directly.&lt;/li>
&lt;/ul>
&lt;p>Useful when your LLM app has no immediate human feedback but you want to know whether its quality has dropped. Apache 2.0, Python.&lt;/p>
&lt;h4 id="whylabs">WhyLabs&lt;/h4>
&lt;p>&lt;a href="https://whylabs.ai/">WhyLabs&lt;/a> is commercial (with whylogs as the underlying OSS library), aimed at enterprise production:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Managed SaaS&lt;/strong> with SOC 2 Type 2 and HIPAA compliance.&lt;/li>
&lt;li>&lt;strong>Real-time monitoring&lt;/strong> via continuous log ingestion.&lt;/li>
&lt;li>&lt;strong>Embedding tracking&lt;/strong>: native support for embedding distributions, not just tabular features.&lt;/li>
&lt;li>&lt;strong>Token probability shifts&lt;/strong>: it monitors the probability distribution of generated tokens, not just metadata.&lt;/li>
&lt;/ul>
&lt;p>For regulated companies that do not want to operate their own drift detection platform, it is the lowest-friction option.&lt;/p>
&lt;h4 id="other-mentions">Other mentions&lt;/h4>
&lt;p>&lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a> (seen in the Evals post) includes drift detection as a module. &lt;a href="https://galileo.ai/">Galileo&lt;/a> has commercial products specialised in LLM monitoring. &lt;a href="https://www.fiddler.ai/">Fiddler AI&lt;/a> and &lt;a href="https://github.com/SeldonIO/alibi-detect">Alibi Detect&lt;/a> (Seldon) are more generalist alternatives that also cover LLMs.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Licence&lt;/th>
&lt;th>Focus&lt;/th>
&lt;th>Typical stack&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Evidently AI&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Drift reports + LLM&lt;/td>
&lt;td>OSS Python, HTML reports&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NannyML&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Performance without GT&lt;/td>
&lt;td>OSS Python, batch&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>WhyLabs&lt;/strong>&lt;/td>
&lt;td>Commercial (whylogs OSS)&lt;/td>
&lt;td>Enterprise SaaS, embeddings&lt;/td>
&lt;td>Continuous logs, compliance&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Arize Phoenix&lt;/strong>&lt;/td>
&lt;td>ELv2&lt;/td>
&lt;td>Tracing + drift unified&lt;/td>
&lt;td>OSS, OTel-native&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Galileo&lt;/strong>&lt;/td>
&lt;td>Commercial&lt;/td>
&lt;td>Premium LLM monitoring&lt;/td>
&lt;td>SaaS, ML expert team&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Alibi Detect&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>General drift detection&lt;/td>
&lt;td>OSS Python, Seldon ecosystem&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Fiddler AI&lt;/strong>&lt;/td>
&lt;td>Commercial&lt;/td>
&lt;td>Explainability + monitoring&lt;/td>
&lt;td>Enterprise SaaS&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="part-3--the-complete-stack-how-everything-fits">Part 3 — The complete stack: how everything fits&lt;/h2>
&lt;p>Let us recap the layers the two series have covered, ordered from closest to the individual request to closest to the aggregate trend:&lt;/p>
&lt;pre tabindex="0">&lt;code>Individual EVENTS Aggregated TRENDS
│ │
Tracing ──→ Evals ──→ Guardrails ──→ MCP obs ──→ Drift detection
│ │
AgentSight ──→ Tetragon ──→ Hubble ──→ eBPF on-device
│ │
(what happens) (what changes)
&lt;/code>&lt;/pre>&lt;p>Each layer answers a different question:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Question it answers&lt;/th>
&lt;th>Granularity&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Tracing&lt;/strong> (Langfuse, AgentSight)&lt;/td>
&lt;td>What exactly did the agent do?&lt;/td>
&lt;td>One session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Evals&lt;/strong>&lt;/td>
&lt;td>Was the answer good?&lt;/td>
&lt;td>One answer&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Guardrails&lt;/strong>&lt;/td>
&lt;td>Is this prompt/answer safe?&lt;/td>
&lt;td>One message&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MCP observability&lt;/strong>&lt;/td>
&lt;td>Which tools did it invoke, at what cost?&lt;/td>
&lt;td>One tool call&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>eBPF on agent/network&lt;/strong> (AgentSight, Hubble)&lt;/td>
&lt;td>How did the system behave?&lt;/td>
&lt;td>Per process/connection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>eBPF on the local engine&lt;/strong> (ProfInfer-like)&lt;/td>
&lt;td>How did the model execute?&lt;/td>
&lt;td>Per runtime function&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Drift detection&lt;/strong>&lt;/td>
&lt;td>Is something changing silently?&lt;/td>
&lt;td>Distribution&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>None of them replaces the others. Full coverage requires all seven. The practical operation:&lt;/p>
&lt;ol>
&lt;li>Layers 1-3 (tracing, evals, guardrails) are mandatory from day one. Any LLM app in production that does not have them is flying blind.&lt;/li>
&lt;li>Layer 4 (MCP) becomes mandatory when there are agents with tools, which is the majority in 2026.&lt;/li>
&lt;li>Layers 5-6 (eBPF) become valuable when the scale justifies the operating cost (&amp;gt;10 services, &amp;gt;100 inference pods).&lt;/li>
&lt;li>Layer 7 (drift) is the one that is most neglected and most expensive to ignore: it takes a day&amp;rsquo;s work to get the basic pipeline and it saves weeks of future incidents.&lt;/li>
&lt;/ol>
&lt;h2 id="the-operational-drift-pattern-in-2026">The operational drift pattern in 2026&lt;/h2>
&lt;p>The minimum recipe any serious LLM app should have:&lt;/p>
&lt;h3 id="step-1--establish-a-baseline">Step 1 — Establish a baseline&lt;/h3>
&lt;p>During a stable post-release period (2 weeks minimum), store:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Embeddings of every prompt&lt;/strong> (vector + metadata: timestamp, user_segment, tenant).&lt;/li>
&lt;li>&lt;strong>Embeddings of the responses&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Automated eval scores&lt;/strong> over a sample (e.g. 5-10% of traffic with G-Eval).&lt;/li>
&lt;li>&lt;strong>The distribution of tools invoked&lt;/strong> (which tools, with which typical arguments, at what frequency).&lt;/li>
&lt;/ul>
&lt;p>Storage: any vector store plus a relational one. Reasonable cardinality at whatever scale you have.&lt;/p>
&lt;h3 id="step-2--continuous-comparison-pipeline">Step 2 — Continuous comparison pipeline&lt;/h3>
&lt;p>Every hour (or every day depending on scale):&lt;/p>
&lt;ul>
&lt;li>Take the sample for the current period (the last hour).&lt;/li>
&lt;li>Apply the statistical tests against the baseline:
&lt;ul>
&lt;li>PSI over simple features (prompt length, tokens, num tools).&lt;/li>
&lt;li>KS over continuous features (latency, score).&lt;/li>
&lt;li>MMD over embeddings.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Generate a drift report (Evidently does it in one line of Python).&lt;/li>
&lt;/ul>
&lt;h3 id="step-3--alerts-and-investigation-workflow">Step 3 — Alerts and investigation workflow&lt;/h3>
&lt;p>Configure thresholds and routes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PSI &amp;gt; 0.25 over tokens consumed&lt;/strong>: moderate alert (it may be legitimate, investigate segments).&lt;/li>
&lt;li>&lt;strong>Significant MMD over prompt embeddings&lt;/strong>: high alert (a change in user mix or a coordinated attack).&lt;/li>
&lt;li>&lt;strong>Eval rubric score down &amp;gt;5% over a rolling 7d&lt;/strong>: critical alert.&lt;/li>
&lt;li>&lt;strong>A new cluster in embedding space accounting for 10%+ of traffic&lt;/strong>: a review workflow (it may be a legitimate new segment or an anomaly).&lt;/li>
&lt;/ul>
&lt;p>Every alert must lead to a drill-down dashboard with the affected segments, not to an empty Slack message. The operational rule: if someone cannot investigate the alert in &amp;lt;5 minutes, it will not get investigated.&lt;/p>
&lt;h3 id="step-4--baseline-refresh">Step 4 — Baseline refresh&lt;/h3>
&lt;p>The baseline is not static. Every N weeks, refresh the baseline by incorporating the &amp;ldquo;new stable&amp;rdquo;. If over 3 months the usage pattern has legitimately changed (more international users, new languages), the baseline must reflect it. The typical cadence: quarterly.&lt;/p>
&lt;h2 id="operational-traps">Operational traps&lt;/h2>
&lt;h3 id="a-contaminated-baseline">A contaminated baseline&lt;/h3>
&lt;p>You take the baseline from a period that already contained the problem in embryo. Result: the baseline includes the bad behaviour and the tests never fire. Solution: verify the baseline against a second independent sample (for example, the first week vs the second) before blessing it.&lt;/p>
&lt;h3 id="a-threshold-that-is-too-low">A threshold that is too low&lt;/h3>
&lt;p>PSI &amp;gt; 0.05 fires constantly. Your team learns to ignore the alerts. Calibrate thresholds against the natural noise of your system: run the system with the baseline plus successive weekly samples and measure the PSI distribution; set the threshold a couple of deviations above normal.&lt;/p>
&lt;h3 id="embeddings-that-are-not-representative">Embeddings that are not representative&lt;/h3>
&lt;p>You use OpenAI&amp;rsquo;s &lt;code>text-embedding-3-small&lt;/code> embedding model to detect drift in a system serving technical questions in Spanish about Cisco networks. Result: the embedding model does not capture the fine semantics of the domain. Solution: use embeddings finetuned for your domain, or one that is strong in multilingual and technical material.&lt;/p>
&lt;h3 id="storage-overload">Storage overload&lt;/h3>
&lt;p>Storing an embedding of every prompt in production at scale (millions of prompts/day) fills disks and increases cost. Stratified sampling: keep 5-10% of traffic, but make sure minority segments are over-represented so you do not lose them.&lt;/p>
&lt;h3 id="confusing-drift-with-the-system-works">Confusing drift with &amp;ldquo;the system works&amp;rdquo;&lt;/h3>
&lt;p>Sometimes drift is good drift: new users discover the agent knows how to do X, and suddenly 30% of traffic is for X. The distribution changed because the product found a new use. Before pulling the alarm, check whether the change is desirable.&lt;/p>
&lt;h3 id="privacy-in-embedding-storage">Privacy in embedding storage&lt;/h3>
&lt;p>Embeddings can be partially inverted back to their original text with embedding inversion techniques. If the prompts contain PII, storing embeddings for months for drift detection is a leak vector. Encrypt at rest and rotate regularly, or work with aggregated/averaged embeddings.&lt;/p>
&lt;h3 id="ebpf-in-production-without-profile-guardrails">eBPF in production without profile guardrails&lt;/h3>
&lt;p>Attaching uprobes to hot-path functions such as &lt;code>llama_decode&lt;/code> can hit throughput if it is not done carefully. Always test in staging and monitor the overhead. ProfInfer reports &amp;lt;4%; what you measure may vary with your binary and kernel.&lt;/p>
&lt;h2 id="closing-the-two-series">Closing the two series&lt;/h2>
&lt;p>This week we have written 12 articles that walk the modern LLM inference stack in production from top to bottom:&lt;/p>
&lt;p>&lt;strong>LLM inference series (4 articles)&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory that holds up LLM inference&lt;/a> — fundamentals.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> — the engine.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention deep dive&lt;/a> — how it works inside.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM Operators on Kubernetes&lt;/a> — orchestration.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>eBPF series (4 articles)&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a> — the substrate.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon&lt;/a> — runtime security.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble&lt;/a> — network observability.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight&lt;/a> — agent observability.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Post-tracing series (4 articles)&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a> — reactive quality.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails&lt;/a> — preventive safety.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability&lt;/a> — the tool protocol.&lt;/li>
&lt;li>This one — drift detection and eBPF in local inference.&lt;/li>
&lt;/ul>
&lt;p>If you read the twelve in order you have a reasonably complete map of what it takes to operate AI agents in serious production in 2026, with enough detail not to crash into the usual problems in the first month. And, above all, with the mindset that LLM observability is a stack, not a product: each layer solves a problem, none of them solves all of them, and the combination is what separates an operable system from one that holds up until the first incident.&lt;/p>
&lt;h2 id="what-is-left-for-future-series">What is left for future series&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>MLOps specific to LLMs&lt;/strong>: continuous fine-tuning, RAG over data lakes, agent training.&lt;/li>
&lt;li>&lt;strong>Constitutional AI and alignment at runtime&lt;/strong>: how the model self-regulates with internal guardrails.&lt;/li>
&lt;li>&lt;strong>GPU networking&lt;/strong>: InfiniBand, NCCL, GPUDirect, the angle we left untouched.&lt;/li>
&lt;li>&lt;strong>Edge inference&lt;/strong>: llama.cpp on phones, MLX on macOS, Snapdragon NPU.&lt;/li>
&lt;li>&lt;strong>Theoretical inference scheduling&lt;/strong>: CFS-like algorithms applied to multi-tenant LLM serving.&lt;/li>
&lt;/ul>
&lt;p>We will cover them in time. That is it for now, and thanks for reading these twelve posts. If they gave you something, share them with a colleague.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;p>eBPF in local inference:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/abs/2601.20755">ProfInfer: An eBPF-based Fine-Grained LLM Inference Profiler (arxiv 2601.20755)&lt;/a> — the 2026 reference paper.&lt;/li>
&lt;li>&lt;a href="https://www.glukhov.org/observability/monitoring-llm-inference-prometheus-grafana/">Monitor LLM Inference in Production 2026 (Glukhov)&lt;/a> — Prometheus + Grafana for vLLM/TGI/llama.cpp.&lt;/li>
&lt;li>&lt;a href="https://www.armosec.io/blog/observability-for-ai-inference-servers/">AI Inference Server Observability in Kubernetes (ARMO)&lt;/a> — the four signals MLOps tools do not capture.&lt;/li>
&lt;li>&lt;a href="https://developers.redhat.com/articles/2025/09/30/vllm-or-llamacpp-choosing-right-llm-inference-engine-your-use-case">vLLM vs llama.cpp: Choosing the right engine (Red Hat)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Drift detection concepts:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://futureagi.com/blog/what-is-llm-drift-2026">What is LLM Drift? Types, Detection, Mitigation 2026 (FutureAGI)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://stackpulsar.com/blog/llm-model-drift-detection/">LLM Model Drift Detection 2026 (Stack Pulsar)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://galileo.ai/blog/best-llm-output-drift-monitoring-platforms">9 Best LLM Drift Monitoring Platforms in 2026 (Galileo)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2404.18673">Open-Source Drift Detection Tools in Action (arxiv 2404.18673)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2309.10000">Detecting covariate drift in text data using document embeddings (arxiv 2309.10000)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://futureagi.com/blog/best-ai-drift-detection-tools-2026">Best AI Drift Detection Tools in 2026 (FutureAGI)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Tools:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/evidentlyai/evidently">Evidently AI (GitHub)&lt;/a> — open-source.&lt;/li>
&lt;li>&lt;a href="https://www.evidentlyai.com/">Evidently — official site&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.nannyml.com/">NannyML&lt;/a> — performance without ground truth.&lt;/li>
&lt;li>&lt;a href="https://whylabs.ai/">WhyLabs&lt;/a> — managed observability.&lt;/li>
&lt;li>&lt;a href="https://github.com/SeldonIO/alibi-detect">Alibi Detect (Seldon)&lt;/a> — general drift detection.&lt;/li>
&lt;li>&lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a> — drift integrated with tracing.&lt;/li>
&lt;/ul>
&lt;p>Statistical tests:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://mlpipeline-cloud.com/blog/data-drift-detection-psi-ks">Data drift detection: PSI vs Kolmogorov–Smirnov (MLPipeline)&lt;/a> — a practical comparison.&lt;/li>
&lt;li>&lt;a href="https://brandonwie.dev/posts/psi-model-drift-detection">Population Stability Index for Model Drift Detection&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.evidentlyai.com/blog/data-drift-detection-large-datasets">Which test is the best? 5 methods to detect data drift (Evidently)&lt;/a> — the 6 hours of advantage for KS.&lt;/li>
&lt;/ul>
&lt;p>Cross-references (the three complete series):&lt;/p>
&lt;ul>
&lt;li>LLM inference series: &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on K8s&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM K8s Operators&lt;/a>.&lt;/li>
&lt;li>eBPF series: &lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight&lt;/a>.&lt;/li>
&lt;li>Post-tracing series: &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>