LLGA · Course notes

ML Ops and LLM Engineering Principles

M2 · P2 (announced) · Year 2 · Period 2

Contents

Introduction and overview

Module ML Ops and LLM Engineering Principles
Code M2 · P2 (announced) — official code not published (course description not published)
Team and workload Instructor: master’s team · ~24 h · from notebook to system: versioning, testing, serving, monitoring, and making LLM-based applications profitable — RAG, fine-tuning, agents — with their latencies, their per-token costs, and their operational guardrails.
Status in the program M2Both routes · preselected by both routes of the home page (Graph × LLM and AI systems) · M2 program being finalized

Audience: an engineer who has a RAG or fine-tuning prototype in a notebook and must ship it.

Empirical first: each chapter starts from a concrete failure — a p95 latency of 4.2 s because the retriever is not batched, a cost multiplied by three after an unversioned prompt change — and only then names the practice that prevents it (tracing, evaluation harness, guardrails).

The module goes from notebook to system: how to version, serve, monitor and control the cost of LLM applications.

What you will be able to do

Official objectives and syllabus

Official course description not published (S3/S4 catalog empty): as of the start of the 2026 academic year, the “LLGA 2 – Semestre 3” and “LLGA 2 – Semestre 4” tracks of the 2026-2027 SynapseS catalog still list no courses, and the master’s wiki indicates that the detailed M2 program is “under development” (period 2: “to be completed soon”).

The program below therefore reflects the master’s brochure (period 2), with an indicative volume of ~24 h; these course materials are calibrated to that volume, i.e. about two hours of work per chapter, labs included.

The following table maps each brochure theme to the course sections; the materials’ own additions are marked “going beyond the syllabus”.

Brochure theme Course section covering it
From notebook to system: lifecycle and versioning of data, features, and models ch. 0 (reminders: what scaling up breaks) and ch. 1 (lineage, reproducibility, artifacts)
CI/CD for ML: testing data and models ch. 2
Serving LLMs: latencies and batching ch. 3 (continuous batching, P99 latency)
Per-token costs: making an application profitable ch. 4 (euros per million tokens; 4.2 break-even of API, fine-tuning, distillation)
Production monitoring: drift and monitoring ch. 5
RAG in production ch. 6 (index freshness, continuous evaluation)
Fine-tuning in production ch. 7 (versioned LoRA adapters, rollback)
Agents and operational guardrails ch. 8 (agent tracing) and ch. 9 (operational security: secrets, sandbox, supply chain)
Going beyond the syllabus — case study of a complete architecture and green ops (carbon cost) ch. 10
Going beyond the syllabus — instrumentation labs (versioning, CI, serving, drift monitoring) Exercises / Labs

Where this course fits in the program

M2 module announced for period 2 and preselected by both routes of the home page (Graph × LLM and AI systems): it is the master’s “systems” course, designed for profiles aiming at engineering as much as research.

Prerequisites: Large Language Models provides the architecture and inference foundations instrumented here, and Deep Learning (P2 variant) raised the training-cost and serving questions now handled in production.

Downstream: this module directly arms the master’s cross-cutting project and the end-of-studies internship — ML/LLM engineer job interviews probe almost word for word its chapters (latency, costs, drift, RAG, agents). It also gives research profiles the vocabulary and constraints for their prototypes to become deployable.

Course outline

Session plan

24 h in class (announced) = 8 sessions of 3 h. The SynapseS 2026–2027 sheet publishes no volume for this module; 24 h is the indicative volume quoted with the brochure programme in the fiche above, and also the M2 volume published for the Large Language Models course (CSC_53432_EP: 24 h, 2 ECTS); the session split below is our proposal for covering these notes in that time. Level 1 = foundations (reviews, definitions, first worked examples), 2 = core methods; the level never goes down from one session to the next.

Session Hours Level Content Practice
1 3 h 1 ch. 0 Reminders: from notebook to system; ch. 1 Lifecycle and versioning Ex. 1
2 3 h 2 ch. 2 CI/CD for ML: data contracts, statistical and paired gates, canary and shadow Ex. 2, 6
3 3 h 2 ch. 3.1–3.2 Continuous batching, KV cache (recall) and PagedAttention Ex. 3
4 3 h 2 ch. 3.3–3.4 Latency SLOs, Little’s law, autoscaling; ch. 4 Costs per million tokens, break-even of API, fine-tuning and distillation Ex. 7, 4
5 3 h 2 ch. 5 Evaluation in production: drift, monitoring, LLM judge Ex. 8
6 3 h 2 ch. 6 RAG Ops; ch. 7 Fine-tuning Ops Ex. 9, 10
7 3 h 2 ch. 8 Agents in production; ch. 9 Operational security Ex. 11, 5
8 3 h 2 ch. 10 Case study and green ops Ex. 12

Self-study, outside the 24 h: no chapter — every chapter is covered in class; the labs each need 1–3 h of personal work after their session, and the common-thread lab (Ex. 12) about 6 h spread over the module.

How to study this module

The LLGA perspective The LLGA master trains profiles able to do research and to put it into production; this module is the pivot of that promise. LLMs are treated here as what they have become in companies: components of a system, with their latencies, their costs, and their failure modes — and the reflexes acquired here (measure, version, monitor) apply equally well to the graph models and tabular pipelines encountered in the other modules.

0. Reminders: from notebook to system

A model that works in a notebook is not yet a service: it must be rebuilt, tested, served, watched and paid for. This chapter recalls what the earlier modules provide and explains what changes when a model becomes a component of a system.

Prerequisites (M1/M2 modules)

  • LLMs (LLMs M2 module): architecture and training (ch. 2-3), SFT, LoRA and distillation (ch. 4), RLHF/DPO (ch. 5), inference and serving with vLLM (ch. 6), RAG (ch. 7), agents (ch. 8), evaluation and safety (ch. 9: judges 9.2, attacks 9.4) — this module extends ch. 6 into engineering;
  • Deep Learning (M1): PyTorch training loops, GPU, checkpoints — the foundation of what must be industrialized;
  • ML (M1): concentration (Hoeffding, Theorem 0.3), subgroup evaluation (4.8) and paired model comparison (4.9) — used in ch. 2 to build non-regression tests;
  • Explainability, Security, Privacy of LLMs (ESP; M2 P2, co-requisite, optional): attacks (ch. 5) and defenses (ch. 6) in depth; the basics used here are in LLMs 9.4;
  • RL (M1): bandits (ch. 4) for A/B tests and model routing;
  • Refresher CS (M1): containers, queues, complexity — the system vocabulary of serving (ch. 3).

The founding observation. A model reaching 92% F1 in a notebook does not “work”: it works in an environment.

The founding paper of the field (Sculley et al., NeurIPS 2015) shows that ML technical debt lives not in the model but around it: data pipelines, glue code, dependency chains, configurations, and feedback loops — the model in production influences the data that will be used to retrain it.

The LLM does not abolish this debt; it adds three specificities: the model is non-deterministic, its input space is all of natural language (hence not boundable by a schema), and its unit cost (the token) is billed on every request.

Definition 0.1 — ML Ops The set of software, data, and systems engineering practices applied to the complete lifecycle of a machine learning system: development, testing, integration, deployment, monitoring, retraining. LLM engineering is the special case where the central component is a language model (API or open weights), typically within a RAG, a fine-tuning, or an agent.

Classical software Traditional ML LLM application
Unit of test Function, API contract Validation set, metric Eval set + judge + prompt contracts
Visible regression Exception, crash Metric drop Fuzzy degradation: style, hallucinations
Inputs Typed, validatable Tabular, schema Natural language + unreliable documents
Execution cost CPU, nearly zero CPU/GPU inference Billed tokens or dedicated GPUs, P99 latency
External dependency Few Batch data Third-party API, vector index, agent tools

Module thread Each chapter contributes one building block of the same application: an enterprise assistant over internal documents (Confluence/SharePoint), with RAG, guardrails, and a budget. Lab 12 assembles it. The references “LLMs module ch. N” point to the M2 module Large Language Models; “Graph ML ch. 10” to the GraphRAG of the M1 module Graph Machine and Deep Learning.

1. Lifecycle: versioning data, features, models

A business RAG assistant passes every demo, then fails on 12% of real queries in production; nobody can say which version of the chunking, the prompt or the index produced the demo results. This chapter makes every element of the system identifiable and replayable, so that such failures can be traced and measured.

What breaks Four mistakes recur across the module: prompts that are not versioned, no regression evaluation before a release (ch. 2), serving without token budgets or timeouts (ch. 3), and judging latency by its mean instead of its tail percentiles (ch. 3).

The ML lifecycle extends well beyond training: collection → labeling → features → training → evaluation → registry → deployment → monitoring → (re)training. Every arrow in this cycle is an artifact that must be identified immutably; failing that, no metric is reproducible — neither for debugging, nor for audits, nor in front of a client.

Definition 1.1 — Artifact and lineage An artifact is any object produced or consumed by the cycle: dataset, feature schema, weights, tokenizer, prompt template, vector index, configuration, container image. The lineage is the graph of artifact → artifact dependencies. A training is reproducible when code + data + configuration + environment are all identified by fingerprints (hashes) and replayable.

Proposition 1.2 — The four fingerprints A training run is reproducible only if one can replay the four components of Definition 1.1: code, data, configuration and environment.

In practice: git versions the code (commit hash), DVC versions the data and the weights (content fingerprint, the living file staying out of the repository), params.yaml links the two, and the container image digest (sha256, last table of this chapter) pins the environment: libraries, CUDA, drivers.

Every published metric references the four fingerprints — otherwise it is not a measurement, it is a rumor.

# Bulky data versioned by content (DVC) -- only the pointer goes into git
$ dvc add data/faq_2026.parquet          # creates data/faq_2026.parquet.dvc (hash + size)
$ git add data/faq_2026.parquet.dvc && git commit -m "faq v3 (dedup + PII scrub)"

# Declarative pipeline: stages re-run if and only if their inputs change
# dvc.yaml
stages:
  prepare:
    cmd: python src/prepare.py data/raw/ data/train.parquet
    deps: [data/raw/, src/prepare.py]
    outs: [data/train.parquet]
  finetune:
    cmd: python src/train_lora.py --config params.yaml
    deps: [data/train.parquet, src/train_lora.py, params.yaml]
    outs: [artifacts/lora_faq_v3/]
  evaluate:
    cmd: python src/evaluate.py --run artifacts/lora_faq_v3/
    deps: [artifacts/lora_faq_v3/, data/eval_v3.parquet]
    metrics: [artifacts/lora_faq_v3/metrics.json:
              {cache: false}]

$ dvc exp run            # replays the chain, records the experiment
$ dvc metrics diff       # compares v2 -> v3

1.1. Feature stores and registries

For tabular ML, a feature store (e.g. Feast) serves the same feature definition offline (training, batch) and online (serving, low-latency), eliminating training/inference skew — the sneakiest class of bug in industrial ML. For LLMs, the functional equivalent is twofold: the prompt cache (shared prefixes; cf. ch. 4) and the embedding bank of the RAG (ch. 6), whose schema (chunk type, embedding model, index version) must be managed like a feature: identified, versioned, replayable.

The model registry (MLflow, or a privatized hf_hub directory) records for each version: the four fingerprints (Proposition 1.2), the qualification metrics, the signature (inputs/outputs), and the status (staging / production / archived). A deployment never references “the latest training” but a frozen version of the registry.

LLM specificity: prompts and evals are artifacts In an LLM application, three families of artifacts add to code, data, configuration and environment: (i) prompt templates (versioned like code, with tests — ch. 2); (ii) evaluation sets (golden sets question → expected answer / relevant documents); (iii) the embedding models and index of the RAG, coupled: changing the embedding model invalidates the whole index (ch. 6). Forgetting to version a prompt is equivalent to not versioning the weights.

Step Artifact Typical tool Fingerprint
Data train/eval sets DVC, lakeFS content hash
Features / embeddings definitions, index Feast, vector database schema version
Training run + weights + LoRA MLflow, HF Hub run id + commit
Evaluation reports, metrics MLflow, RAGAS eval set hash
Deployment container image OCI registry sha256 digest
Serving logs, traces OTel, Langfuse model + prompt + index

2. CI/CD for ML: testing data and models

The continuous integration of an ML system tests four layers, in order of increasing cost: the code, the data, the model, the infrastructure. The golden rule: make the pipeline fail as early as possible, at the cheapest level.

2.1. Data tests

Data is an interface like any other: one writes contracts for it. A data contract checks: the schema (types, columns, bounds), business invariants (allowed nulls, ranges), and statistical properties (duplication rate, distribution of a key column). Tools: Pandera (contracts in Python, executed in the pipeline), Great Expectations (declarative validation suites), quality servers integrated into platforms.

import pandera as pa
import pandas as pd

schema = pa.DataFrameSchema({
    "question": pa.Column(str, pa.Check.str_length(3, 2000)),
    "answer":   pa.Column(str, pa.Check.str_length(1, 8000), nullable=True),
    "lang":     pa.Column(str, pa.Check.isin(["fr", "en"])),
    "doc_ids":  pa.Column(pa.List(str), pa.Check.list_length(1, 10)),
    "n_tokens": pa.Column(int, pa.Check.ge(0), pa.Check.le(4096)),
})

def validate(df: pd.DataFrame) -> pd.DataFrame:
    return schema.validate(df, lazy=True)   # collects ALL errors

2.2. Model tests — and prompt tests

Definition 2.1 — Model non-regression test Let a fixed qualification set D_{\text{qual}} and a metric m (F1, accuracy rate, refusal rate, mean cost). The candidate f_{\text{new}} replaces f_{\text{prod}} if m(f_{\text{new}}) \geq m(f_{\text{prod}}) - \tau, where \tau is the tolerance margin.

The subtle point: m is measured on a sample — deciding with a measured gap requires knowing when it is significant.

Proposition 2.2 — Decision threshold by concentration (Hoeffding) Let \ell be a loss bounded in [0,1], measured on n i.i.d. examples. With probability \geq 1-\delta (on both sides): \bigl|\;\mathbb{E}[\ell] - \widehat{\ell}_n\;\bigr| \;\leq\; \sqrt{\frac{\log(2/\delta)}{2n}}.

To decide a rollback at \delta = 0.05 on n = 1000 examples, the mean loss of one model is known to within \sqrt{\log 40 / 2000} \approx 0.043 . For the per-example difference of two models, which lies in [-1, 1], the bound doubles to \approx 0.086.

Two models whose mean losses differ by less than that are not distinguishable by this test: the observed gap may be sampling noise.

Hoeffding’s bound uses only the range of the loss; a paired test that uses the observed variance of the differences is usually much tighter.

Proof. The Hoeffding bound (Machine Learning, Theorem 0.3) applies to the n i.i.d. variables \ell_i \in [0,1]: \mathbb{P}\bigl(|\overline{\ell} - \mathbb{E}\ell| \geq t\bigr) \leq 2e^{-2nt^2}. Setting the right-hand side to \delta yields t = \sqrt{\log(2/\delta)/(2n)}.

To compare two models one applies it to the variable \ell_{\text{new}} - \ell_{\text{prod}}, bounded in [-1,1]: for a variable of range r, Hoeffding’s bound reads 2e^{-2nt^2/r^2}, so a range twice as large doubles t.

With n=1000 and \delta = 0.05: t \approx 0.043 for one model, t \approx 0.086 for the difference. ∎

Practical consequence A “+0.4% F1” measured on 500 examples proves nothing: the 95% confidence interval is on the order of ±4 points for a metric at 50%. CI gateways must therefore require a minimal gap (margin \tau and significance), under pain of “dancing on the noise” — a rollback here, a rollback there, and a slow drift of quality.

Hoeffding’s bound is a worst case. The gate actually run in CI pairs the two models item by item, because both models fail on the same hard items and that shared noise cancels in the difference.

Definition 2.3 — Paired non-regression gate Score both models on the same n qualification items and keep the per-item loss differences \delta_i = \ell_i(f_{\text{new}}) - \ell_i(f_{\text{prod}}), \qquad i = 1, \dots, n . With \bar\delta their mean and s_\delta their standard deviation, the candidate passes if the upper end of a one-sided 95 % interval stays below the margin: \bar\delta + 1.645\, \frac{s_\delta}{\sqrt n} \;\le\; \tau . For a metric that is not a per-item mean (F1) or is far from normal (judge win rate, RAGAS score), use instead the 95th percentile of the metric difference recomputed on 10^4 bootstrap resamples of the items.

Worked example — n = 1000 right/wrong items; the candidate loses 40 items that production gets right and wins 30 that production gets wrong.

These are the paired comparisons of Machine Learning 4.9 and LLMs 9.6, the evaluation kit shared by every Year 2 project.

Model tests also come in slices: performance per language, per context length, per topic, per user category — a global aggregate can mask a localized drop (the subgroup evaluation of Machine Learning 4.8). For an LLM, prompt tests add on: output format (valid JSON, schema), refusal guarantees (forbidden phrases), determinism wrappers (temperature 0, seed) and an LLM judge as oracle (ch. 5) — the judge itself being versioned and tested on a golden set.

# .github/workflows/eval-gate.yml (excerpt) -- CPU gates first, GPU last
jobs:
  unit:                      # 1. code: fast tests, no model
    steps: [run: pytest tests/unit -x -q]
  data:                      # 2. data: pandera contracts on the day's delta
    steps: [run: python -m src.validate_delta --date $(date -d yesterday +%F)]
  eval:                      # 3. model: golden set + judge + prompt contracts
    steps:
      - run: python -m src.eval_llm --model staging --golden data/eval_v3.parquet
      - run: python -m src.gate --min-faithfulness 0.85 --tau 0.02 --paired bootstrap  # Def. 2.3
  deploy:                    # 4. progressive deployment (canary 5% -> 50% -> 100%)
    needs: [unit, data, eval]
    steps: [run: ./deploy.sh --canary 5]

2.3. Continuous delivery: canary and shadow

The deployment of an ML component is progressive: shadow (the new model receives duplicated traffic, its output is logged but not served — zero risk, double cost), canary (5% of traffic, automatic rollback if the guard metrics — latency, error rate, negative feedback — degrade), then full rollout. Rollback must be a single button: the previous version (weights + prompt + index) stays deployable in one command, thanks to the registry of ch. 1.

3. LLM serving: continuous batching, P99 latency

A served LLM must answer many users at once, fast enough and at an acceptable cost per token. This chapter explains how a server batches requests, why the memory of the KV cache is the real limit, and how latency is specified and measured.

Reminder — LLMs module ch. 6 (inference)

Autoregressive decoding generates one token per forward pass; attention requires retaining the keys/values of all past tokens (KV cache); quantization reduces the weights and the KV; the per-token complexity grows with the context length. This chapter moves from “how it computes” to “how it holds a load”.

3.1. Batching: from one request to N

A GPU executes a decoding step in a time nearly independent of the number of sequences present in the batch (as long as memory and bandwidth allow): the operations are parallel and the GPU is memory-bound on the weights. Serving B sequences simultaneously therefore divides the cost per token by almost B — this is the economic argument for shared serving.

Naive batching (static batching) waits to have B requests, processes them as a block, and leaves the GPUs idle as soon as a short sequence finishes: yet a 20-token request ends quickly, and the batch becomes sparse.

Definition 3.1 — Continuous batching (Orca, OSDI 2022) Schedule serving at the iteration level rather than the request level: at every decoding step, finished sequences leave the batch and waiting requests enter it. The GPU no longer idles between two requests. Combined with selective batching (batched operations where possible, per-request loops elsewhere), Orca claims up to 36.9× the throughput of prior systems at equal latency.

# Continuous batching loop (simplified) -- the heart of every modern LLM server
running, queue = [], deque()

def step():
    # 1) decoding: one iteration for ALL active sequences (a single GPU call)
    outputs = model.forward_batch([s.tokens for s in running])
    # 2) remove finished sequences (EOS or max length)
    for s in [s for s in running if s.finished(outputs)]:
        running.remove(s); s.finalize()
    # 3) admit new requests as long as the KV memory allows
    while queue and kv_pool.has_room(queue[0].prompt_len):
        running.append(queue.popleft().prefill())
    # 4) preemptive scheduling: if the KV saturates, evict (recompute/swap)

3.2. Memory: the KV cache and PagedAttention

Recall 3.2 — KV-cache size (stated in LLMs, Proposition 2.4)

For a transformer with n_{\text{layers}} layers and n_{\text{kv}} key/value heads (GQA) of dimension d_{\text{head}}, in precision b bytes, the cache of a sequence of T tokens occupies: M_{\text{KV}} \;=\; 2 \cdot n_{\text{layers}} \cdot n_{\text{kv}} \cdot d_{\text{head}} \cdot b \cdot T \;\text{bytes}. The count is direct: each layer stores, for each token, one key and one value of n_{\text{kv}} d_{\text{head}} coefficients each, at b bytes per coefficient. Hence the factor 2, and only the K/V heads count, however many query heads there are.

Example (Llama-3-8B: n_{\text{layers}}=32, n_{\text{kv}}=8, d_{\text{head}}=128, fp16 b=2): 2\times32\times8\times128\times2 = 131,072 bytes ≈ 128 KiB per token, i.e. 1 GiB for 8,192 tokens — as much memory as the weights of a small quantized model.

Continuous batching therefore makes memory occupancy oscillate at every iteration (sequences entering/leaving). Allocating the KV as one maximal contiguous block (maximum output length reserved in advance) wastes.

vLLM (SOSP 2023) treats the KV cache like the virtual memory of an OS — pages of 16 tokens, allocated on demand, non-contiguous, shareable between sequences derived from the same prefix (system prompt shared by thousands of requests): waste reduced to ~4% versus more than 60% for static allocation, throughput 2-4× prior systems.

Ops consequence: the real capacity of a server is measured in “available KV tokens”, not in concurrent requests.

3.3. Latency: TTFT, TPOT, P99

Definition 3.3 — LLM serving SLO TTFT (time to first token): request → first token delay — dominated by the prefill (prompt) and the waiting queue. TPOT (time per output token): generation cadence — dominated by decoding and batch contention. P99: 99th percentile of a latency distribution — the relevant contractual commitment; the average masks queues. A typical interactive assistant SLO: TTFT P99 < 1.5 s, TPOT P99 < 80 ms/token.

Theorem 3.4 — Little’s law In a stable system in steady state, with L average clients present, \lambda clients arriving per second on average and W mean sojourn time:

Key result — Little’s law L \;=\; \lambda\, W.

Serving corollary: at an imposed generation cadence (W fixed by the number of tokens and the TPOT), the number of concurrent requests supported is L = \lambda W — the necessary concurrency grows linearly with the targeted throughput.

Proof. Proof by an output argument (direct). Over an interval [0, t], let A(t) be the number of arrivals and D(t) the number of departures. The mean number of clients present is written L_t = \frac{1}{t}\int_0^t N(u)\,du, where N(u) = present at instant u.

Each request contributes to this integral exactly its sojourn time: \int_0^t N(u)\,du = \sum_{i \,\text{departed by} \leq t} W_i + \text{(residuals of requests still present)}.

Dividing by t and letting t\to\infty (stability: A(t)/t \to \lambda, bounded residuals become negligible), one obtains L = \lambda\, W — the argument requires no independence or distribution assumption; it is purely accounting. ∎

Throughput versus latency: the batch trade-off Increasing B (target batch) improves throughput (amortized cost/token) but lengthens the waiting queue — hence the TTFT P99. Modern servers expose this trade-off: max_num_seqs, token budgets per step, and preemption policies (recompute vs swap). Rule of thumb: size the target batch so that KV occupancy stays below ~90%, and let the autoscaler (section 3.4) absorb spikes rather than growing the batch beyond the SLO.

3.4. Autoscaling

The relevant signal for scaling an LLM server is not CPU but: the waiting queue length (requests pending admission), the mean waiting time (TTFT − prefill time) and the KV occupancy rate. Horizontal autoscaling (HPA on a custom Prometheus metric) adds replicas; watch out for warm-up spikes (weight loading: tens of seconds to minutes — pre-pull the images, or pay the P99). For multi-GPU models (tensor parallelism, Deep Learning P2, ch. 2), each replica is a group of GPUs; scaling changes the granularity of parallelism, not just the number of pods.

# Launch a vLLM server exposing an (OpenAI-compatible) API on port 8000
$ docker run --gpus all -p 8000:8000 vllm/vllm-openai:v0.6 \
    --model meta-llama/Meta-Llama-3-8B-Instruct \
    --max-num-seqs 256 --gpu-memory-utilization 0.92 \
    --enable-prefix-caching            # reuse the KV of shared prefixes

# Async benchmark: TTFT / TPOT P50-P99 as a function of concurrency
import asyncio, json, time, httpx

async def one(client, prompt, n_out=256):
    t0 = time.perf_counter(); first = None; n = 0
    async with client.stream("POST", "/v1/chat/completions",
            json={"model": "...", "messages": [{"role": "user", "content": prompt}],
                  "max_tokens": n_out, "stream": True}) as r:
        async for line in r.aiter_lines():
            if not line.startswith("data:") or line.strip() == "data: [DONE]":
                continue
            choices = json.loads(line[5:]).get("choices") or [{}]
            if choices[0].get("delta", {}).get("content"):  # skip the empty role chunk
                if first is None:
                    first = time.perf_counter() - t0          # TTFT
                n += 1                                        # content chunks (~ tokens)
    if first is None or n < 2:
        return first, None                                    # no stream: count it as a failure
    # after the first token there are n - 1 inter-token gaps, not n
    return first, (time.perf_counter() - t0 - first) / (n - 1)   # (TTFT, TPOT)

async def main(conc=64):
    async with httpx.AsyncClient(base_url="http://localhost:8000",
                                 timeout=300) as c:
        res = await asyncio.gather(*[one(c, "Summarize this report in 5 points.")
                                     for _ in range(conc)])
    p = lambda x, q: sorted(x)[int(q * len(x))]
    ttft = [r[0] for r in res if r[0] is not None]
    tpot = [r[1] for r in res if r[1] is not None]
    print(f"failed streams: {conc - len(tpot)}")
    if not tpot:
        return                                                # every stream failed
    print(f"conc={conc}  TTFT p50/p99 = {p(ttft,.5):.2f}/{p(ttft,.99):.2f}s"
          f"  TPOT p99 = {p(tpot,.99)*1e3:.0f}ms")

for conc in (1, 4, 16, 64, 256): asyncio.run(main(conc))
Metric Definition Dominated by Typical target (chat)
TTFT P50 / P99 first token prefill, queue < 0.4 / < 1.5 s
TPOT P99 ms/token afterwards decoding, contention < 80 ms
Throughput (tokens/s/GPU) aggregated output batch size, KV available maximize under SLO
KV occupancy % pages used concurrency × length < 90%
Rejected requests timeouts / 429 saturation, slow autoscale ~0

4. Costs: euros per million tokens

Every LLM architecture decision sooner or later reduces to a cost per million tokens (Mt). APIs bill the input (prompt: amortizable by caching) and the output (generation: expensive) separately; the 2025-2026 orders of magnitude roughly span, from the small open-weight model to the frontier model, from a few cents to a few tens of euros per Mt — rates that change several times a year: any precise figure in course materials would be stale; the method of computation, on the other hand, remains.

Proposition 4.1 — Unit cost of self-hosted serving If a server delivers a sustained throughput of R tokens/s (outputs, continuous batching) on GPUs costing c €/h (rental all inclusive), the operational cost per million output tokens equals:

Key result — cost per million tokens \text{cost/Mt} \;=\; \frac{c}{R} \cdot \frac{10^6}{3600} \;\approx\; 278 \cdot \frac{c}{R} \;\; \text{€/Mt}.

Example: an 8B on a GPU at ~2 €/h delivering R = 2,000 tokens/s in batch → ≈ 0.28 €/Mt; the same GPU on isolated requests (batch 1, R \approx 60) → ≈ 9 €/Mt.

Batching is a factor of 30 on the bill.

Proof. The server produces R tokens per second; each billed hour at c € therefore produces 3600\,R tokens; the cost per token is c/(3600\,R) €, and per million tokens 10^6 c/(3600 R) = 277.8\, c/R . The direct division by R: doubling the throughput halves the cost per token at constant hardware — this is the economic justification of ch. 3. ∎

4.1. The saving levers

Criterion Managed API Self-hosted (open weights)
Low-usage cost Pay-per-token, optimal Brutal GPU amortization
High-usage cost Linear, becomes expensive ~0.1-1 €/Mt depending on batch (prop. 4.1)
Sensitive data Contract, jurisdiction, zero-retention to negotiate Total control (closed network)
P99 latency The provider’s Controllable (ch. 3), at your own expense
Ability to fine-tune Limited (hosted LoRAs depending on the offering) Total (ch. 7)
Ops Nearly zero Team + on-call + GPU supply

Multilingual and real costs The per-token bill interacts with tokenization (cf. LLMs module ch. 1): a French corpus typically costs 15-25% more tokens than an equivalent English corpus with the dominant tokenizers. The “price per word” therefore depends on the language — an argument for measuring (exercise 4) rather than trusting round conversion tables.

4.2. API, fine-tuning or distillation: the break-even volume

The table compares two ways of serving; the next question is which model to serve. An API model is paid per request. A model of your own, fine-tuned or distilled, adds a one-off training cost and a fixed GPU bill, then costs almost nothing per extra request. The methods are taught in LLMs 4.1 and 4.3; this section covers their economics.

Proposition 4.2 — Break-even volume An API request costs a € (its input and output tokens at their prices). An own model costs F € per month of reserved GPUs, a training cost C_{\text{train}} amortized over H months before retraining, and b < a € per request at the margin. Over a month of V requests, the own model is cheaper as soon as V \;\ge\; V^\star = \frac{F + C_{\text{train}}/H}{a - b}.

Proof. The two monthly bills, aV and F + C_{\text{train}}/H + bV, are affine in V with slopes a > b, so they cross once, at V^\star. Below it the API is cheaper; above it the own model is, provided its quality passes the gate of ch. 2. ∎

Worked example — 10 k requests a day (300 k a month), 1,500 input tokens (RAG context) and 300 output tokens, with illustrative API prices of 1 €/Mt in and 4 €/Mt out:

5. Evaluation in production: drift and monitoring

A model that passed every test at release time degrades silently when the world it serves changes. This chapter defines the kinds of drift, gives the statistics that detect them, and organizes monitoring and LLM-based evaluation in production.

5.1. Data drift, concept drift

Definition 5.1 — Drift Data drift: the distribution of inputs changes (P(X) moves — new topics, new user population, new documentary source). Concept drift: the input → correct answer relation changes (P(Y\mid X) moves — yesterday’s “good” answer becomes wrong: regulation, company merger, vocabulary). For a RAG, a third drift: the corpus changes (ch. 6).

Proposition 5.2 — PSI = Jeffreys divergence The population stability index (PSI), summed over the bins i of a histogram: \mathrm{PSI}(p, q) \;=\; \sum_i \bigl(p_i - q_i\bigr)\, \ln\frac{p_i}{q_i} \;=\; \mathrm{KL}(p \,\|\, q) + \mathrm{KL}(q \,\|\, p), is exactly the Jeffreys divergence: symmetric, zero iff p = q.

Usual thresholds in credit risk: PSI < 0.1 stable; 0.1-0.25 to watch; > 0.25 alert.

Proof. Let us expand: \mathrm{KL}(p\|q) = \sum_i p_i \ln(p_i/q_i), \qquad \mathrm{KL}(q\|p) = \sum_i q_i \ln(q_i/p_i). The sum equals \sum_i (p_i - q_i)\ln(p_i/q_i) — precisely the PSI.

Each term is positive (the two factors p_i - q_i and \ln(p_i/q_i) have the same sign); zero if and only if p_i = q_i for all i. Symmetry is obvious from the summed form. ∎

The classic detectors: PSI/KL per feature (interpretable, bin by bin), the Kolmogorov–Smirnov test for continuous features, the MMD distance between feature distributions (Graph ML 9.6, which uses it to compare generated graphs), and stream detectors (ADWIN) for online signals.

Kolmogorov–Smirnov in three lines. Compare a reference window of n values with a current window of m values through the largest gap between their empirical distribution functions: D = \sup_x \bigl|\widehat F_{\text{ref}}(x) - \widehat F_{\text{cur}}(x)\bigr| . Under “same continuous law” and for large windows, D\sqrt{nm/(n+m)} approximately follows the Kolmogorov distribution, whose 95 % quantile is about 1.36: alert when D > 1.36\,\sqrt{(n+m)/(nm)}.

For an LLM, the “features” to monitor are: prompt length, language, topics (lightweight classifier), presence of injected documents, truncation rate — and output proxies: refusal rate, “I don’t know” rate, median answer length, JSON parse failure rate.

Without online labels, one monitors symptoms; the true metrics come from feedback and sampling.

5.2. Monitoring: three time loops

Loop Latency Signals Action
Reactive second-minute latency, 5xx errors, 429, KV saturation, cost/req autoscale, throttle, rollback
Daily day PSI per feature, refusal rate, feedback 👎, sampled RAGAS eval ticket, investigation, prompt fix
Strategic week-month concept drift (refreshed golden set eval), cost drift, adoption retraining/fine-tune, model migration

The metrics of an LLM application stack up: infra (GPU, memory); serving (ch. 3); quality (explicit feedback, sampled evals); safety (guardrail trigger rate, injection attempts — cf. ch. 8 and Explainability, Security, Privacy of LLMs, ch. 5).

The structural watchpoint: the feedback loop — the data collected in production (real questions, feedback) become tomorrow’s training data: the model steers the distribution it will later see (selection bias, rich-get-richer). Documenting this loop is part of the audit (ch. 9; group fairness in Machine Learning 4.8).

5.3. The LLM judge in CI and production

Judge: recall and ops consequences A judge model evaluates (question, answer) pairs against a rubric (fidelity to context, completeness, tone). Zheng et al. (MT-Bench, NeurIPS 2023) show agreement with humans > 80% on conversation tasks; its biases — position, verbosity, self-preference — and their mitigations are listed in LLMs 9.2.

Ops consequences: alternated order, explicit rubric, reference provided, human golden set to calibrate the judge, judge version frozen in CI (ch. 1). The judge is a noisy sensor — one uses it to detect regressions, not to certify.

# Mini regression judge in CI: compares prod vs candidate on the golden set
def judge_pair(question, ref, ans_prod, ans_cand, judge):
    rub = "Expected answer (reference): {ref}\nA: {a}\nB: {b}\n" \
          "Which one is more faithful to the reference? Answer A, B, or EQUAL."
    first = judge(f"Q: {question}\n" + rub.format(ref=ref, a=ans_prod, b=ans_cand)).strip()
    second = judge(f"Q: {question}\n" + rub.format(ref=ref, a=ans_cand, b=ans_prod)).strip()
    # alternated order, anti-bias: the candidate wins only if it wins in both positions
    if first.startswith("B") and second.startswith("A"):
        return "cand"
    if first.startswith("A") and second.startswith("B"):
        return "prod"
    return "equal"            # EQUAL, or inconsistent verdicts (position bias)

# gate: no significant regression. "equal" verdicts carry no evidence either way,
# so test prod wins against candidate wins on the decisive pairs only (sign test,
# the logic of McNemar's test in section 2.2)
from scipy.stats import binomtest
verdicts = [judge_pair(**ex, judge=judge) for ex in golden]
w_cand, w_prod = verdicts.count("cand"), verdicts.count("prod")
n_dec = w_cand + w_prod
p = binomtest(w_prod, n_dec, 0.5, alternative="greater").pvalue if n_dec else 1.0
assert p >= 0.05, f"judge regression: prod {w_prod} vs cand {w_cand}, p = {p:.3f}"

A fixed threshold on candidate wins alone would count every tie against the candidate: a candidate identical to production would get no wins and fail. The sign test ignores ties and fails only when production wins significantly more decisive pairs.

6. RAG Ops: freshness and continuous evaluation

A RAG system is only as good as its index, and the documents behind the index change every day. This chapter keeps the index fresh without rebuilding it, and evaluates answer quality continuously without a human in the loop.

Reminder — LLMs module ch. 7 (RAG)

Pipeline: chunking → embeddings → vector index → retrieval (hybrid BM25 + dense, cf. NLP ch. 2 and 7) → reranking → conditioned generation. This chapter treats that pipeline as a data system with freshness and evaluation contracts — not as a demo.

6.1. Incremental indexing and freshness

The enterprise corpus moves: Confluence pages edited, contracts signed, FAQs enriched. The index must follow with an explicit freshness SLA (“a published document is queryable within 15 minutes”). The standard pattern: change data capture (webhooks/API of the document repository) → event queue → incremental upsert into the index, key = (source, doc_id, version).

Subtleties:

6.2. Evaluating a RAG without humans: RAGAS

Definition 6.1 — RAGAS metrics (Es et al., 2023, and the ragas library) Es et al. define three reference-free metrics, computed by LLM decomposition: faithfulness (is every claim of the answer attested by the context? — anti-hallucination), answer relevance (does the answer answer the question?) and context relevance (how much of the retrieved context is actually needed?).

The open-source ragas library adds two metrics that compare the retrieval with a ground-truth reference answer: context precision (are the relevant chunks ranked first?) and context recall (are the necessary chunks retrieved?).

The context metrics isolate the retrieval, faithfulness and answer relevance the generation: this is the diagnostic grid missing from a global F1.

from ragas import evaluate
from ragas.metrics import (faithfulness, answer_relevancy,
                           context_precision, context_recall)
from datasets import Dataset

ds = Dataset.from_dict({
    "question":     q_list,          # golden set (ch. 1: version this set!)
    "answer":       answers,         # outputs of the candidate pipeline
    "contexts":     retrieved_chunks,
    "ground_truth": ref_answers,     # reference: context_precision and context_recall
})
report = evaluate(ds, metrics=[faithfulness, answer_relevancy,
                               context_precision, context_recall])
# CI gate: faithfulness >= 0.85; weekly panel: the 4 time series

Reading a RAGAS report like an engineer Low recall, high faithfulness: retrieval is failing — improve chunking, hybrid, reranker. High recall, low faithfulness: the model invents despite the context — prompt, stronger model, forced citation. Low relevancy: off-topic or verbose answer. The trap of reference-free metrics: their judge has the biases of ch. 5 — one calibrates them on a hand-annotated sample before making them CI thresholds.

6.3. The graph to the rescue of RAG

Vector RAG fails on global questions (“what are the recurring themes of the 2025 reports?”) that require aggregating the whole corpus; GraphRAG (Edge et al., 2024; cf. Graph ML module ch. 10) builds an entity graph (LLM extraction), partitions it into communities (Leiden) and summarizes each community — the global answer becomes a map-reduce over the summaries.

The ops cost: LLM indexing is expensive (every document goes through extraction calls), hence incremental by necessity; communities are recomputed periodically.

The arbitration (vector vs graph vs hybrid) belongs in the system’s ablation table — and must be re-measured at every model migration (ch. 2).

Freshness of a graph index or KG snapshot A GraphRAG index, or a knowledge-graph snapshot fed to the model, is a derived artifact with its own version, and it ages differently from a vector index.

  • Local vs global parts: entity and relation extraction is incremental per document, keyed by (doc_id, version) as in 6.1; communities and their summaries are global, since one new document can merge or split several communities.
  • Two freshness numbers: the SLA states when a document is retrievable by vector search (minutes) and when it reaches graph-based answers (after the next community run, e.g. nightly). Re-summarize only the communities whose membership changed.
  • Lineage: the snapshot id (extraction model and prompt hash, community run, date) goes into the registry (ch. 1) and every trace (ch. 8), like the vector index version.
  • Deletion: removing a document removes its triples and invalidates the summaries that cited it — the right to erasure applies to derived artifacts too.

7. Fine-tuning Ops: versioned LoRAs

Fine-tuning produces small adapters that are trained often and deployed on a shared base model; without strict bookkeeping, an adapter ends up running on the wrong base or with the wrong prompt. This chapter defines what must be versioned and organizes the fine-tuning cycle in production.

Reminder — SFT and DPO (LLMs module, ch. 4–5), and LoRA

Instruction fine-tuning (SFT, LLMs module ch. 4) adapts the model. LoRA (Hu et al., ICLR 2022), introduced in LLMs 4.3, makes this affordable: it freezes the weights W and learns only \Delta W = BA of rank r \ll d: a few tens of MB instead of GB, trainable on a single GPU. DPO/RLHF (LLMs module, ch. 5) then align the preferences. This chapter is the operations view of LoRA.

Definition 7.1 — Versioned adapter An adapter (LoRA) is usable only when coupled with: (i) its exact base (name + version of the weights); (ii) its prompt template; (iii) its training data (hash); (iv) its hyperparameters (r, \alpha, target modules).

The unit of versioning is not the .safetensors file but this quintuple — a LoRA on the “wrong” base (same architecture, different checkpoint) produces silently degraded outputs.

# Adapter registry: the record is the authority (MLflow / private HF Hub / simple CSV)
adapter:        faq-v3-lora
base:           meta-llama/Meta-Llama-3-8B-Instruct@9c6b4c1d   # exact hash
template:       prompts/faq_chat_v2.jinja                      # git version
data:           data/faq_2026.parquet @ sha256=4f2a...         # DVC
lora_config:    {r: 16, alpha: 32, target: [q_proj, v_proj]}
metrics:        {eval_v3_f1: 0.91, faithfulness: 0.88, judge_winrate: 0.56}
trained_by:     pipeline#2026-03-11, run_id 7c3e...
deployed:       canary 5% since 2026-03-13, SLO OK

7.1. The fine-tuning cycle in production

  1. Collect: feedback 👍/👎, expert corrections, questions without a good answer (failed queries from monitoring — ch. 5);
  2. Clean / deduplicate / anonymize: deduplication reduces memorization (Explainability, Security, Privacy of LLMs, ch. 7);
  3. Build the pairs (SFT) then preferences (DPO) — balance of domains, slices evaluated separately (ch. 2);
  4. Train LoRA on the frozen base; log the quintuple;
  5. Qualify: golden set + judge + safety tests (fine-tuning can degrade refusals!) — gate of ch. 2;
  6. Deploy in canary with rollback (ch. 2.3);
  7. Measure the impact (winrate, cost, latency) before generalization — fine-tuning must win against simply improving the prompt and against RAG, otherwise it has no reason to exist.

Multi-LoRA serving Use case: a single 8B engine serves ten clients, each with its own LoRA (tone, domain, language). Modern servers load several adapters on a base shared in memory — a single copy of the weights, the \Delta W (a few tens of MB) being applied per request.

The ops work consists of guaranteeing the routing (client header → adapter) and the isolation of evaluations per tenant (slices of ch. 2). The base rarely changes; the adapters often: two distinct deployment cadences in the same pipeline.

Proposition 7.2 — Size of a LoRA A rank-r LoRA on a layer W \in \mathbb{R}^{d_{\text{out}} \times d_{\text{in}}} stores B \in \mathbb{R}^{d_{\text{out}} \times r} and A \in \mathbb{R}^{r \times d_{\text{in}}}, i.e. r\,(d_{\text{out}} + d_{\text{in}}) \text{ parameters instead of } d_{\text{out}} d_{\text{in}}.

On the q and v projections of the 32 layers of an 8B model (d = 4096; with GQA the v projection has output dimension 1024) at r = 16: 16 \times (4096 + 4096) + 16 \times (4096 + 1024) \approx 2.1 \times 10^5 parameters per layer, about 6.8 M in total — ~0.1% of the parameters, about 14 MB in fp16.

The storage saving is a bonus; the decisive saving is the training cost (no optimizer state on the frozen weights) and the deployment cadence (adapter hot-swap).

8. Agents in production: tracing and guardrails

An agent makes dozens of model and tool calls per request, and any of them can fail, loop or be hijacked. This chapter makes agents observable, bounds what they can spend and do, and explains how to evaluate them.

Reminder — LLMs module ch. 8 (agents)

An agent = an LLM in a loop (observation → reasoning → action via tools), with memory and possibly multi-step planning. This chapter adds no intelligence: it adds observability, the budgets and the permissions without which an agent is undebuggable and dangerous.

8.1. Tracing: the tree of calls

An agent request unfolds in dozens of nested LLM and tool calls. The central datum is the trace: a tree of spans (one span per call: input, output, token cost, latency, model, attempt), OpenTelemetry-compliant — the vendors Langfuse/W&B Weave plug into it.

Without a trace: no ability to answer “why did the agent send this email?”. With a trace: deterministic replay (by logging the seeds/times), bottleneck analysis (which tool dominates the latency?), per-step costs, and detection of behavioral anomalies (loops: same tool, same arguments, N times).

// OTel span of an agent call (excerpt) -- everything is there for replay and cost
{
  "trace_id": "a1b2...", "span_id": "17", "parent_id": "03",
  "name": "llm.call",
  "attributes": {
    "llm.model": "llama3-8b-instruct@v3-lora-faq",
    "llm.prompt_hash": "9e21...",         // -> versioned template (ch. 1)
    "llm.input_tokens": 1842, "llm.output_tokens": 96,
    "llm.cost_eur": 0.00042, "llm.latency_ms": 741,
    "agent.step": 4, "agent.tool": "search_confluence",
    "rag.index_version": "2026-03-11.2"
  }
}

8.2. Guardrails: in layers, around the loop

Layer Mechanism Example
Input validation, injection classifiers, quota block 500 kB prompts, injection detector (LLMs 9.4; ESP ch. 5-6)
Loop maximum budgets max 15 steps, max 0.10 €/task, global timeout
Action per-tool permissions, scoping read-only on Confluence; sending e-mails = human confirmation
Output format and content validators strict JSON schema; no bank details in the answers
Execution sandbox (ch. 9) code run in an ephemeral container with no outbound network
Observation behavioral alerts loop rate, success per task, cost drift

Example 8.1 — The budget as the first guardrail A monitoring agent performs on average 6 LLM calls + 3 tool calls per task. A malicious user (or a booby-trapped document — indirect injection, LLMs 9.4) can make it loop: each iteration re-injects a bit of context. Cap per task: steps ≤ 15, tokens ≤ 50 k, cost ≤ 0.10 € — and cut off cleanly (degraded answer: “I could not finish”): the failure becomes a bounded cost instead of a billing incident.

Proposition 8.2 — Per-task success collapses with length If each step succeeds independently with probability p, a task of k steps succeeds with p^k: p = 0.95 and k = 20 give 36%.

Hence: (i) shorten the chains (more reliable tools, shallower plans); (ii) local retries and reflexivity; (iii) evaluate agents per complete task, never per step.

8.3. Evaluating an agent

Agent metrics: success rate per task (on scripted tasks with an initial state and a success criterion — e.g. “the ticket is created with the right fields”), average cost per task (tokens × price + tools), end-to-end P95 latency, human intervention rate. The evaluation environments must be replayable: simulated tools (mock Confluence, mock mail) for CI, a sampled real environment for production — the simulation runs hundreds of tasks for a few cents.

Definition 8.3 — Error budget (Beyer et al., Site Reliability Engineering, 2016) An SLO with target s over a window of N requests or tasks allows (1 - s)\,N failures: this allowance is the error budget. While budget remains, releases go ahead; once it is spent, releases freeze and the team works on reliability until the window rolls over.

For an agent, the SLO is stated per complete task. With a 95 % task-success SLO and 10,000 tasks a month, the budget is 500 failed tasks. Proposition 8.2 tells how fast a longer plan spends it: at p = 0.997 per step, a 10-step plan fails about 3 % of tasks (some 300 failures, within budget), a 20-step plan about 5.8 % (some 580, over budget) — unless its steps become more reliable.

9. Operational security: secrets, sandbox, supply chain

An LLM application holds credentials, executes generated code and depends on weights and libraries downloaded from elsewhere; each is an entry point for an attacker. This chapter secures the system around the model.

Scope The attacks against the model (jailbreaks, injections, poisoning, extraction) are introduced in LLMs 9.4 and treated in depth by the M2 P2 co-requisite Explainability, Security, Privacy of LLMs (ESP, ch. 5–6). Here: the security of the system around the model — the ops of an LLM application; the reference checklist of the field is the OWASP Top 10 for LLM Applications.

9.1. Secrets and identities

9.2. Sandbox and isolation

When the agent executes code (notebooks, “calculator” tools, plugins), execution must be confined: an ephemeral container, a temporary file system, outbound network blocked by default, CPU/RAM limits, a hardened runtime (e.g. gVisor/Firecracker for kernel-level isolation), and filtering of exfiltrations — including over slow channels (DNS queries, timing).

An agent compromised by injection will try to exfiltrate via the only channel it was left (the tools! the mail! the displayed output?). Hence the duo permissions (ch. 8) + sandbox: the same principle as in system security — compromise is likely, blast radius minimal.

9.3. Supply chain

Ops threat Vector Countermeasure
API key theft repo, image, logs vault + rotation + provider quotas
Exfiltration by the agent tool, mail, code scoped permissions, egress deny, sandbox
Payment denial (bill shock) agent loop, spam budgets (ch. 8), per-client quotas
Booby-trapped model backdoored weights provenance + hash + safety evals (ch. 2)
Leak via logs verbose traces PII scrubbing, short retention, logged access

10. Case study and green ops

The previous chapters each solved one problem; a real service must solve them all at once, within a budget in euros and in energy. This chapter assembles the full stack of an enterprise assistant and shows how to measure and reduce its energy cost.

10.1. The stack of an enterprise assistant

Let us assemble the bricks. Specifications: 2,000 employees, SSO, Confluence + SharePoint corpus (~500 kB documents), questions in French, sourced answers, no data outside the perimeter, annual budget of the order of a few tens of k€ of inference.

Component Typical choice Chapter Points of attention
Auth & gateway SSO OIDC, per-user quotas ch. 9 identity = the key to agent permissions
ingestion CDC Confluence → chunks → embeddings ch. 6 idempotence, 15 min freshness SLA
Index vector + BM25 (hybrid) ch. 6 versioned, dual-boot during migration
Reranker light cross-encoder ch. 6 +30 ms, −20% of context errors typically
Generation front model API or self-hosted 8B ch. 3-4 routing according to document sensitivity
Adaptation LoRA “internal tone” ch. 7 rollback = adapter swap
Agents search + writing, read permissions ch. 8 budgets, tracing, sandbox
Evaluation golden set of 500 Q/A + weekly RAGAS + CI judge ch. 2, 5, 6 versioned set, slices by business line
Monitoring Prometheus + OTel + alerting ch. 3, 5, 8 P99 TTFT, PSI, cost/req, refusal rate

Why not GraphRAG? If the dominant usage becomes “which teams work on topic X?” (relational questions), the entity graph (Graph ML module ch. 10; GraphRAG of Edge et al.) becomes profitable despite its indexing cost: the answer to the question “do we need a graph?” is a measured trade-off (RAGAS eval + ablation), not a fad.

10.2. Green ops: measuring energy

The energy cost of LLMs concentrates (i) in pre-training — Patterson et al. (2021) formalize the carbon accounting of large trainings (consumption, regional electricity mix, PUE); Luccioni et al. (2022) do the same for BLOOM including the amortization of training; (ii) in aggregated inference — billions of requests of a few Wh each.

For a developer, the useful unit is the consumption per request: measurable with CodeCarbon (CPU/GPU/PUE meter) around each serving point.

from codecarbon import EmissionsTracker

with EmissionsTracker(measure_power_secs=1, log_level="warning") as tracker:
    answer = rag_pipeline.answer(question)      # full request

print(f"{tracker.total_energy_kWh * 1000:.1f} Wh for this request",
      f"({tracker.total_emissions * 1e3:.1f} gCO2e on the local mix)")

Proposition 10.1 — The four green levers, in order of effectiveness

  1. Functional sobriety: routing to small models, shorter outputs, cache (ch. 4) — divides the energy without touching the hardware.
  2. Quantization: ~2-4× on the energy throughput per token (cf. LLMs ch. 6).
  3. Batching: the same GPU delivers more tokens per Wh in batch (ch. 3). Proposition 4.1 holds with the power drawn, P watts (GPU, host share and PUE), in place of the price c €/h, since P watts for one hour is P Wh: \text{energy/Mt} \;=\; \frac{P}{R} \cdot \frac{10^6}{3600} \;\approx\; 278 \cdot \frac{P}{R} \;\; \text{Wh/Mt}. At P = 500 W and R = 2{,}000 tokens/s this is about 70 Wh/Mt; at batch 1 (R \approx 60) and the same power, about 2.3 kWh/Mt — the factor of 30 of the bill. A lightly loaded GPU draws less power, so the real energy factor is somewhat smaller.
  4. Geographic and temporal scheduling: moving the nightly batches to the low-carbon-mix regions at the right moment.

The order matters: one does not buy green GPUs before having shortened the outputs.

LLGA position This module is deliberately transversal to the heart of the master: serving (ch. 3) extends the LLMs module, graph RAG (ch. 6) extends the Graph ML module, agents (ch. 8) extend the RL module, and evaluation in production (ch. 5) prepares the safety track of the Explainability, Security, Privacy module. The instructors of this proposed course are not yet confirmed: the cited standards and tools are authoritative; the LLGA (LLMs & Graphs) group of the X is mobilized when relevant.

Exercises / Labs

  1. ★ Reproducibility (ch. 1). Build a mini DVC pipeline (prepare → embed → eval) over 100 documents; modify a single document and predict which stages re-execute; verify. Show that a metric without the four fingerprints (code, data, configuration, container digest) is not replayable.
  2. ★ Data contracts (ch. 2). Write a Pandera schema for a FAQ set (question/answer/language/tokens) and make CI fail on: exact duplicate, empty answer, unknown language, abnormal length. Add the error report as an artifact.
  3. ★ Serving (ch. 3). Deploy an 8B on vLLM; plot the TTFT P50/P99 and TPOT P99 curves for concurrency 1 → 256; locate the breaking point (KV occupancy); compare static batching (vary it) and continuous batching.
  4. ★ Accounting (ch. 4). For a given workload (short questions, 300-token answers, 10 k req/day), quantify: API (input × in price, output × out price, prefix cache), self-hosted (proposition 4.1 with R measured in lab 3). Identify the crossover point in requests/day (Proposition 4.2) and its sensitivity to the utilization rate.
  5. ★ Ops audit (ch. 9). Put a mini LLM application through the sieve of the OWASP Top 10 LLM: list the findings, severity, fixes; check in particular: secrets, tool permissions, output validation, log retention.
  6. ★★ Significance of a gate (ch. 2). Simulate two models with losses 0.30 and 0.31 on n = 500; how many times does the “no regression” test wrongly fail over 100 draws? Deduce from it a realistic minimal margin for your CI. Repeat with the paired gate of Definition 2.3 when the two models share 90 % of their errors, and compare the margins.
  7. ★★ Little’s law in practice (ch. 3). From the traces of lab 3, estimate \lambda, W and L over a 10 min window; verify L = \lambda W within ±10%; explain the gap (non-stationarity, preemption).
  8. ★★ Drift (ch. 5). Generate two weeks of simulated “logs” with a topic drift on day 9; implement PSI (prop. 5.2) and KS over the lengths and topics; tune the threshold for ≤ 1 false alert per week; document the detection delay / false alerts trade-off.
  9. ★★ RAG Ops (ch. 6). Take the RAG of the LLMs module ch. 7: move to incremental indexing (adding 20 documents at D+1 without full re-indexing); measure RAGAS before/after; trigger an embedding migration and run the index dual-boot.
  10. ★★ Versioned LoRAs (ch. 7). Train two LoRAs (r = 8, 32) on the same set; record the quintuples; serve both in multi-LoRA; evaluate; deliberately “forget” the base version and observe the silent degradation — then add the compatibility check to the registry.
  11. ★★ Observable agent (ch. 8). Instrument a search + writing agent with OTel spans; inject three failures (a tool that times out, a retry loop, a too-long context); diagnose each one only through the traces; add budgets and caps, replay.
  12. ★★★ Common-thread lab (ch. 10). Assemble the assistant: hybrid RAG + reranker + LoRA + guardrails + dashboard (latency, PSI, RAGAS, cost/req, Wh/req via CodeCarbon); present a “simulated post-mortem” (a game day): another team injects a faithfulness regression, and your team must detect it (dashboard?) and roll back (registry) in less than 15 minutes.

Projects and assessment

Official assessment: not displayed. The SynapseS 2026–2027 sheet of this module is not published (the LLGA 2 – S3/S4 lists are empty; see the fiche), so it gives no modality, weighting or resit rule. Everything below is a proposal: a project, solo or in a group, plus a written exam, weighted as the instructor decides. Each subject can also serve as the operations work package of a Year 2 capstone (end-of-year projects). The projects exercise O7 (hands-on and industry practice, the core), O3 (LLMs in production) and O8 (guardrails, security, drift); the first group subject adds O5 (graph × LLM).

Solo projects

  1. [Solo · ≈ 20 h] How many false rollbacks? Statistically sound evaluation gates. Which gate blocks real regressions without “dancing on the noise”, and can an LLM judge be trusted as its sensor? A paper study of Zheng et al., “Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena” (NeurIPS 2023).
    • Implement: four gates — Hoeffding (Proposition 2.2), the paired gate (Definition 2.3, normal and bootstrap), McNemar, and Wald’s sequential probability ratio test (1945). Measure the false-rollback rate and the detection power against n, on simulated correlated losses and on real model pairs.
    • Data: per-item outcomes of small open models (for example Qwen2.5-1.5B-Instruct against its own 4-bit version, a near-tie, and against Qwen2.5-0.5B-Instruct, a clear regression) on GSM8K (Cobbe et al., 2021) and MMLU (Hendrycks et al., 2021), logged by lm-evaluation-harness; the MT-Bench human judgments released with the paper, to measure a judge’s position and length biases.
    • Deliverables: a small gate library with tests, a 4–6 page report, a 10-minute talk. Chapters: ch. 2, 5.3.
    • Success: error-rate curves with intervals that match the theory on simulations; the judge’s agreement with humans measured with and without ties, as in the paper; the sign-test gate of 5.3 shown to pass an identical candidate and to block a worse one.
  2. [Solo · ≈ 20 h] An LLM serving capacity and cost planner. How many GPUs, and which server settings, does a given traffic need to hold a P99 SLO at the lowest €/Mt and Wh/Mt? A paper study of Kwon et al., “Efficient Memory Management for Large Language Model Serving with PagedAttention” (SOSP 2023). It extends solo subject 2 of Large Language Models from one server to a fleet.
    • Implement: benchmark an 8B model on vLLM (the code of 3.4) across concurrency, max_num_seqs, prefix caching and 4-bit quantization; check Recall 3.2 against the measured KV occupancy and Little’s law on the traces (Ex. 7); fit a throughput–latency model; derive €/Mt and Wh/Mt curves (Propositions 4.1 and 10.1).
    • Simulate: an autoscaler driven by queue length and KV occupancy (3.4), on a replayed day of arrivals, against a TTFT P99 < 1.5 s SLO.
    • Data: Llama-3.1-8B-Instruct in AWQ int4 on a 16 GB T4 (free Colab or Kaggle), or Qwen2.5-1.5B-Instruct in fp16; ShareGPT prompts, as in vLLM’s benchmark scripts; arrival times from the Azure LLM inference trace released with Splitwise (Patel et al., ISCA 2024).
    • Deliverables: benchmark and planner code, a 4–6 page report, a 10-minute talk. Chapters: ch. 3, 4, 10.2.
    • Success: measured KV memory within 10 % of Recall 3.2; Little’s law within 10 % (Ex. 7); the planner’s predicted TTFT P99 within 20 % of a held-out load test.
  3. [Solo · ≈ 20 h] A drift observatory for a chat assistant. Which detector sees a topic or population shift in real chat logs first, at no more than one false alarm per week? A paper study of Rabanser, Günnemann & Lipton, “Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift” (NeurIPS 2019).
    • Implement: PSI with smoothing (Proposition 5.2), KS tests with a Bonferroni correction over features, MMD on sentence embeddings, and ADWIN (Bifet & Gavaldà, SDM 2007) on the output proxies of 5.1 (refusal rate, answer length, JSON failures).
    • Data: WildChat-1M (Zhao et al., ICLR 2024), a year of real chatbot conversations with timestamps; inject controlled drifts (a new topic, a language mix, longer prompts) on top of the natural ones.
    • Deliverables: code, an alert dashboard, a 4–6 page report, a 10-minute talk. Chapters: ch. 5, 6.1. Stretch: wire an alert to a LoRA retraining cycle with canary rollback (7.1, 2.3).
    • Success: detection delay against false alarms per week, per detector, over 20 injected drifts with intervals; the paper’s finding — univariate KS tests on a reduced representation compete with multivariate MMD — confirmed or refuted on text.

Group projects

  1. [Group of 4 · ≈ 30 h each] Ship a GraphRAG enterprise assistant under SLOs. Can the common-thread assistant (10.1, Ex. 12) hold its SLOs, freshness SLA and budget under replayed traffic and document edits — and survive a game day? The game day is the W4 red-team swap of the Year 2 transverse project.
    • Implement: a hybrid vector + GraphRAG index kept fresh by change data capture (6.1 and the graph-freshness note of 6.3); CI gates from RAGAS and the order-swapped judge (Definition 2.3, 5.3); a multi-LoRA tone adapter (ch. 7); OTel tracing and an SLO dashboard with TTFT P99, PSI, cost and Wh per request (ch. 3, 5, 8).
    • Data: the public GitLab Handbook (CC BY-SA 4.0), whose git history replays real document edits; a 300-question golden set written by the team (local, multi-hop and global questions), 30 % held out.
    • Other modules: GraphRAG from Graph ML ch. 10; RAG (ch. 7) and LoRA (4.3) from Large Language Models.
    • Split: ingestion and indexes; evaluation and gates; serving, adapters, cost and energy; observability and the game day.
    • Deliverables: a repository with one-command deployment, the dashboard, an 8-page cost and carbon report, a live game day in which another team injects a regression. Chapters: ch. 1–8, 10.
    • Success: TTFT P99 within the SLO under replayed load; the freshness SLA met on replayed edits; the injected regression detected and rolled back in under 15 minutes; cost and Wh per request within 25 % of Propositions 4.1 and 10.1.
  2. [Group of 3 · ≈ 30 h each] Hardening a tool-using agent: from red team to ops. How much attack success can operational guardrails remove from a tool-using agent, and at what cost in utility? A paper study of Debenedetti et al., “AgentDojo: A Dynamic Environment to Evaluate Prompt Injection Attacks and Defenses for LLM Agents” (NeurIPS 2024, Datasets and Benchmarks).
    • Implement: an OTel-instrumented agent (8.1) on the AgentDojo suites (workspace, banking, travel, Slack), with an open tool-calling model served by vLLM or an API model. The red team runs indirect injections and exfiltration attempts (LLMs 9.4, ESP ch. 5); the blue team adds per-task budgets, scoped tool permissions, an egress-denied sandbox and output validators (8.2, ch. 9).
    • Other modules: attacks and defenses from Explainability, Security, Privacy of LLMs, ch. 5–6; the threat model of Security and Robustness, ch. 1.
    • Split: red team; blue team; ops (tracing, cost, and CI regression tests built from every successful attack).
    • Deliverables: a repository, an OWASP Top 10 LLM audit (Ex. 5), a 6–8 page report, a 15-minute talk with a live attack. Chapters: ch. 2, 8, 9.
    • Success: utility and attack success rate before and after each defense, with intervals; per-task success set against p^k (Proposition 8.2); cost per task; three failures diagnosed from the traces alone (Ex. 11).

Written exam (proposal)

Format: 2 hours; one handwritten A4 sheet and a non-programmable calculator allowed; 4 questions — a course question, a computation, a short proof and a drift exercise.

  1. Question (5 points). A team releases a new prompt template with a new LoRA adapter for the enterprise assistant. (a) List the artifacts and fingerprints the release must record (2 pts). (b) Give the CI layers in order, the deployment stages and the rollback unit (2 pts). (c) On 100 decisive pairs, the order-swapped judge prefers the candidate 55 times: may the team claim an improvement (1 pt)?

    Expected:

      1. The four fingerprints (code commit, data hash, configuration, container digest); the prompt-template version; the exact base-model hash, adapter data hash and hyperparameters (Definition 7.1); the evaluation-set hash and the judge version.
      1. Code tests, data contracts, model and prompt tests on the golden set with the paired gate, then infrastructure; shadow, canary at 5 %, full rollout. The rollback unit is a registry version (weights, adapter, prompt, index), redeployed in one command.
      1. No. Under “no difference”, 55 or more wins out of 100 has probability about 0.18, so 55 % is within the noise; ties carry no evidence either way.
  2. Question (6 points). A model has 32 layers and 8 KV heads of dimension 128 (fp16 cache); 60 GiB of GPU memory are left for the cache. Requests have 1,500 prompt and 300 output tokens; TTFT is 0.5 s and TPOT 50 ms. (a) KV bytes per token and KV tokens available (1.5 pts). (b) Maximum concurrent requests, each counted at full length (1 pt). (c) With Little’s law, the largest sustainable arrival rate (1.5 pts). (d) At that rate, the cost per million output tokens and per request on a GPU rented at 3 €/h (2 pts).

    Expected:

      1. 2 \times 32 \times 8 \times 128 \times 2 = 131{,}072 bytes, i.e. 128 KiB per token; 60 \times 2^{30} / 2^{17} = 491{,}520 tokens.
      1. 491{,}520 / 1{,}800 \approx 273 requests.
      1. W = 0.5 + 299 \times 0.05 \approx 15.5 s (299 gaps after the first token), so \lambda \le 273 / 15.5 \approx 17.6 requests/s. Check: 273 sequences at 20 tokens/s each decode about 5,460 tokens/s, close to R = 17.6 \times 300 \approx 5{,}280 tokens/s.
      1. Proposition 4.1 gives 278 \times 3 / 5{,}280 \approx 0.16 €/Mt; per request, 3 / (17.6 \times 3600) \approx 4.7 \times 10^{-5} €, about 5 cents per thousand requests.
  3. Question (5 points). Two models are scored on the same n i.i.d. items.

      1. The per-item losses have variances \sigma_1^2 and \sigma_2^2 and correlation \rho. Show that the mean paired difference has variance (\sigma_1^2 + \sigma_2^2 - 2\rho\sigma_1\sigma_2)/n, and say when pairing helps (2 pts).
      1. For right/wrong scores, let a count the items only the candidate gets right, b those only production gets right, and m = a + b. Show that under “equal accuracy”, conditionally on m, a \sim \mathrm{Bin}(m, 1/2); deduce McNemar’s statistic (a - b)^2/(a + b) (2 pts).
      1. Apply it with a = 30 and b = 40 (1 pt).

    Expected:

      1. The differences \delta_i are i.i.d. with variance \sigma_1^2 + \sigma_2^2 - 2\rho\sigma_1\sigma_2, and averaging n i.i.d. terms divides the variance by n. Pairing helps when \rho > 0, i.e. when both models fail on the same hard items; independent test sets would give \rho = 0.
      1. Equal accuracy forces the two kinds of discordance to be equally likely, so each discordant item favours either model with probability 1/2, independently: a has mean m/2 and variance m/4. Hence z = \frac{a - m/2}{\sqrt{m}/2} = \frac{a - b}{\sqrt{a + b}} is approximately standard normal, and z^2, McNemar’s statistic, approximately follows \chi^2_1.
      1. \chi^2 = 100/70 \approx 1.43 < 3.84: no significant difference at 5 % (p \approx 0.23), as in the worked example of 2.2.
  4. Question (4 points). A prompt-length feature is split into three bins. The reference week has proportions (0.5, 0.3, 0.2), the current week (0.3, 0.3, 0.4). (a) Compute the PSI and classify it with the usual thresholds (2 pts). (b) What happens if a bin is empty in the current week, and how is it fixed (1 pt)? (c) Which monitoring loop of 5.2 handles this signal, and with what action (1 pt)?

    Expected:

      1. 0.2 \ln(5/3) + 0 + 0.2 \ln 2 \approx 0.102 + 0.139 = 0.24: “to watch” (0.1–0.25), just under the alert threshold.
      1. The term \ln(p_i/0) is infinite, and so is the PSI. Smooth the histograms (add a small \epsilon to every bin) or merge sparse bins.
      1. The daily loop: open a ticket and investigate (a new user population? a new document source?), and confirm on the refreshed golden set before any retraining.

Grading rubric (proposal)

Criterion Weight What earns the marks
Question, SLOs and baselines 15 % One precise question; SLOs, budgets and a simple baseline (API model, vector-only RAG, no guardrail) fixed before any optimization
Correct implementation 20 % Code that runs with one command; formulas checked on a toy case (KV memory against Recall 3.2, PSI against a hand computation)
Measurement and statistics 25 % Tail percentiles rather than means; paired intervals and gates (Definition 2.3); several runs; false-alarm and detection rates measured, not assumed
Operations quality 15 % Lineage with the four fingerprints, CI gates that actually block, rollback in one command, traces that explain every failure shown
Cost, energy and safety 10 % €/Mt and Wh/Mt measured and set against Propositions 4.1, 4.2 and 10.1; attacks and failure modes reported honestly
Report, dashboard and talk 15 % A clear 4–8 page report, a readable dashboard, a talk or game day that answers the question within the time

References