LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnMath & StatisticsDistributions and Sampling
📊EasyEvaluation & Benchmarks

Distributions and Sampling

Match binary outcomes, routes, tool-call counts, and latency to first distributions, then reject a simulation that doesn't fit the traces.

31 min read
Learning path
Step 17 of 196 in the full curriculum
Statistics and UncertaintyHypothesis Tests, Intervals, and pass@k

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A small incident assistant helps engineers with CI failures, rollbacks, access requests, and log triage. A launch review asks for 5,000 synthetic requests before traffic arrives. Each request contributes four measurements: it either resolves or escalates, takes one route, makes a nonnegative number of tool calls, and takes a positive number of seconds.

Now choose how to draw those four values. A wrong choice can make an average look reasonable while hiding retry bursts, producing impossible negative latency, or inventing a route that never appears in the taxonomy. Before trusting a simulator, name the claim behind each draw and the trace that could falsify it.

In Statistics and Uncertainty, 16 abuse labels out of 100 reviewed flags became a point estimate plus an interval. That asked how much a finite sample supports one unknown rate. Simulation reverses the direction: start with a process, draw new samples, and ask whether the resulting traces look like the system you intend to model.

A probability distribution specifies the allowed values of a random quantity and how probability is assigned to them. We'll start with those allowed values, then check the frequencies, tails, and dependencies that a simulator produces.

The numbers below are planning assumptions, not measured production results. A real launch would estimate them from logged, reviewed traffic. The standard-library examples need Python 3.12 or later because they use Random.binomialvariate. Record the interpreter version along with the seed; distribution-sampling algorithms can change between versions.[1]Reference 1random: Generate pseudo-random numbershttps://docs.python.org/3/library/random.html

Four measurements from one request

Quantity from one requestExample valueValue shapeFirst simulation modelSummary to report
Resolved without a human1 or 0binaryBernoulliresolution rate
Request routeci_failure, rollback, access_request, log_triageone labelcategoricallabel mix
Tool calls made0, 1, 2, ...countPoisson baselinemean and share above a cost threshold
End-to-end response time7.4 secondspositive continuouslognormal baselinemedian and p95

The first simulation model column is a claim about shape, not a default. If traces disagree, you revise the model instead of defending the convenient formula.

Discrete values versus continuous time

The first split is between discrete and continuous quantities.

KindMeaningIn the incident assistant
Discretevalues are separate outcomes you can enumerateresolved or escalated, one intent label, number of tool calls
Continuousvalues live on a measured rangeresponse time in seconds

For a discrete quantity you can ask for the probability of one exact value, such as P(tool_calls = 2). For a continuous response time, one infinitely precise instant has probability zero under the model. Ask for a range or a percentile instead: P(time > 15 seconds) or the p95 response time.

The allowed values are the distribution's support. Route labels need a finite set of names, tool calls need nonnegative integers, and response time needs nonnegative seconds. A measured zero may mean a duration was rounded below the clock's resolution; a negative duration is a different problem.

Before sampling, write down four things:

  1. What values are allowed?
  2. Which parameter sets the rate, center, or spread?
  3. Which tail or threshold would hurt users or cost?
  4. What observation would prove this model is a poor fit?

For example, ten tool calls may be rare under a baseline but common during retry loops. That observation should change the model, not just the seed.

PMF, PDF, and CDF

Use the kind of quantity to choose the probability object. For the binary resolution variable R, the two masses are P(R = 0) = 0.28 and P(R = 1) = 0.72. A probability mass function (PMF) lists those probabilities for each discrete value. The masses add to 1.

Response time needs different language. A probability density function (PDF) describes how densely probability is spread across possible times; area over an interval gives its probability. Its height isn't the probability of one exact time and may exceed 1. For instance, a uniform duration between 0 and 0.5 seconds has density 2 per second, but total area 2 × 0.5 = 1.

A cumulative distribution function (CDF) gives the chance of finishing by a deadline. Let T mean response time and t the chosen deadline:

FT(t)=P(T≤t)F_T(t) = P(T \leq t)FT​(t)=P(T≤t)

Read the CDF as an operations question: F_T(10) is the chance a response finishes within 10 seconds, while 1 - F_T(15) is the chance it exceeds 15 seconds. For the smooth latency model below, p95 is the time where the CDF reaches 0.95. Discrete distributions have CDFs too, but those rise in steps and can jump past 0.95.[2]Reference 2Machine Learning: A Probabilistic Perspective.https://probml.github.io/pml-book/book0.html

A chart or API that says "probability" is easy to misread. A PMF bar is a probability. A PDF curve needs an interval before it becomes one. A CDF's height is already accumulated probability.

The probability chapter treated a distribution as a function over a known sample space. Here you pick a named family, sample from it, and check whether traces match the family.

Inverse CDF transform: how computers sample continuous variables

How does a random-number generator turn deterministic code into smooth, continuous response times? A computer's core RNG primitive only knows how to produce uniform numbers between zero and one: U∼Uniform(0,1)U \sim \text{Uniform}(0, 1)U∼Uniform(0,1).

The mathematical bridge from uniform numbers to any continuous distribution is the inverse CDF transform (also known as the probability integral transform). Because a continuous CDF F(t)=P(T≤t)F(t) = P(T \leq t)F(t)=P(T≤t) is strictly increasing from 0 to 1, its inverse F−1(u)F^{-1}(u)F−1(u) exists. If you draw U∼Uniform(0,1)U \sim \text{Uniform}(0, 1)U∼Uniform(0,1), the transformed variable T=F−1(U)T = F^{-1}(U)T=F−1(U) follows the target distribution FFF exactly:

P(F−1(U)≤t)=P(U≤F(t))=F(t)P(F^{-1}(U) \leq t) = P(U \leq F(t)) = F(t)P(F−1(U)≤t)=P(U≤F(t))=F(t)

To see how that works analytically, consider an exponential service duration with completion rate μ\muμ:

F(s)=1−e−μs=u  ⟹  e−μs=1−u  ⟹  s=−ln⁡(1−u)μF(s) = 1 - e^{-\mu s} = u \implies e^{-\mu s} = 1 - u \implies s = -\frac{\ln(1 - u)}{\mu}F(s)=1−e−μs=u⟹e−μs=1−u⟹s=−μln(1−u)​

Because 1−u1 - u1−u is also distributed uniformly on (0,1)(0, 1)(0,1), code can draw u∼Uniform(0,1)u \sim \text{Uniform}(0, 1)u∼Uniform(0,1) and compute s=−ln⁡(u)/μs = -\ln(u) / \mus=−ln(u)/μ. For a lognormal variable with median eμe^\mueμ and log-spread σ\sigmaσ, you invert standard normal noise: draw uuu, find the standard normal quantile z=Φ−1(u)z = \Phi^{-1}(u)z=Φ−1(u), and exponentiate: t=exp⁡(μ+σz)t = \exp(\mu + \sigma z)t=exp(μ+σz).

Distribution families and continuous inverse CDF transform sampling. Top cards define discrete PMF (point masses summing to 1), continuous CDF (accumulating probability F(t)), and inverse transform sampling (mapping uniform noise u in (0,1) to duration t = F⁻¹(u)). On the left, a Bernoulli PMF shows 28 percent escalation and 72 percent resolution. On the right, a continuous lognormal CDF plot demonstrates inverse transform sampling: a uniform draw at u = 0.50 projects to 6.0 seconds (median), and u = 0.95 projects to 14.8 seconds (p95).
On the left, discrete PMF bars represent exact outcome probabilities. On the right, inverse CDF transform sampling turns uniform random numbers u ∈ (0, 1) on the vertical axis into simulated durations on the horizontal axis: u = 0.50 yields the 6.0-second median, and u = 0.95 yields the 14.8-second p95 latency.

Notice the geometry in the plot: where the CDF is steep (near the 6-second median), a wide swath of uniform draws projects into a narrow slice of time. Where the CDF flattens out (the right tail), a tiny 5% sliver of uniform draws (u∈[0.95,1.0]u \in [0.95, 1.0]u∈[0.95,1.0]) stretches out across all latencies beyond 14.8 seconds.

Before running the snippet below, predict the output: 5,000 inverted uniform draws should land very close to the theoretical 0.05-second exponential mean, the 6.0-second lognormal median, and the 14.83-second p95 threshold.

sample-via-inverse-cdf.py
1from math import exp, log 2from random import Random 3from statistics import NormalDist, median 4 5rng = Random(42) 6rate = 20.0 7uniform_draws = [rng.random() for _ in range(5_000)] 8exp_samples = [-log(1.0 - u) / rate for u in uniform_draws] 9 10mu = log(6.0) 11sigma = 0.55 12standard_normal = NormalDist(0, 1) 13lognormal_samples = [ 14 exp(mu + sigma * standard_normal.inv_cdf(u)) for u in uniform_draws 15] 16 17print(f"exponential mean: {sum(exp_samples) / len(exp_samples):.3f}s (target: {1/rate:.3f}s)") 18print(f"lognormal median: {median(lognormal_samples):.2f}s (target: 6.00s)") 19p95_index = int(0.95 * len(lognormal_samples)) 20print(f"lognormal p95: {sorted(lognormal_samples)[p95_index]:.2f}s (target: 14.83s)")
Inverse CDF transform sampling
1exponential mean: 0.050s (target: 0.050s) 2lognormal median: 6.03s (target: 6.00s) 3lognormal p95: 14.85s (target: 14.83s)

Bernoulli: resolved or escalated

Begin with the smallest distribution. Let R = 1 if the assistant resolves a request without a human, and R = 0 if it escalates. The planning value is p = 0.72.

OutcomeRProbability
escalation00.28
resolved10.72

The expected value is:

E[R]=0×0.28+1×0.72=0.72E[R] = 0 \times 0.28 + 1 \times 0.72 = 0.72E[R]=0×0.28+1×0.72=0.72

Because R is encoded as 0 or 1, its long-run average is the resolution rate. Calculate that weighted average directly:

bernoulli-resolution-rate.py
1p_resolved = 0.72 2outcomes = {"escalated": 0, "resolved": 1} 3probabilities = {"escalated": 1 - p_resolved, "resolved": p_resolved} 4 5expected_value = sum( 6 outcomes[name] * probabilities[name] for name in outcomes 7) 8 9print(f"P(resolved): {probabilities['resolved']:.2f}") 10print(f"P(escalated): {probabilities['escalated']:.2f}") 11print(f"E[R]: {expected_value:.2f}") 12 13assert expected_value == p_resolved
Bernoulli resolution setup
1P(resolved): 0.72 2P(escalated): 0.28 3E[R]: 0.72

One request is now a coin with a known bias. The next question is what happens when you count many of those coins.

Binomial: resolutions in a 100-request batch

If you count resolutions across n = 100 independent requests that all share p = 0.72, the count X has a binomial distribution. A single request is Bernoulli; the sum of these 100 outcomes is binomial.[3]Reference 3NIST/SEMATECH e-Handbook: Binomial Distributionhttps://www.itl.nist.gov/div898/handbook/eda/section3/eda366i.htm

Its expected count is np = 72, and its variance is np(1-p) = 20.16. The count can land anywhere from 0 to 100, even though 72 is the center of the model.

One particular arrangement of 72 resolutions and 28 escalations has probability 0.72^72 × 0.28^28. There are many arrangements with that same count. The binomial coefficient (10072)\binom{100}{72}(72100​) counts them, giving:

P(X=72)=(10072)(0.72)72(0.28)28≈0.089P(X=72) = \binom{100}{72}(0.72)^{72}(0.28)^{28} \approx 0.089P(X=72)=(72100​)(0.72)72(0.28)28≈0.089

The binomial model is a sum of independent Bernoulli trials. If requests arrive in correlated incident waves, or if each request has a different resolution probability, this simple count model needs a richer design.

Before running the snippet, predict its shape: six rates should scatter around .72, not all equal .72. The code checks the formula, then draws six independent 100-request batches while holding p = 0.72 fixed. Random.binomialvariate is the standard-library binomial sampler.

sample-resolution-batches.py
1from math import comb 2from random import Random 3 4n = 100 5p = 0.72 6k = 72 7p_exact = comb(n, k) * (p**k) * ((1 - p) ** (n - k)) 8 9print(f"P(X={k}) = {p_exact:.3f}") 10print(f"E[X] = {n * p:.0f}") 11print(f"Var(X) = {n * p * (1 - p):.2f}") 12 13rng = Random(12) 14resolved_per_batch = [rng.binomialvariate(n, p) for _ in range(6)] 15rates = [count / n for count in resolved_per_batch] 16 17print("resolved counts:", resolved_per_batch) 18print("batch rates: ", [round(rate, 2) for rate in rates]) 19print(f"range: {min(rates):.2f} to {max(rates):.2f}")
Resolution batches wobble
1P(X=72) = 0.089 2E[X] = 72 3Var(X) = 20.16 4resolved counts: [72, 70, 75, 69, 71, 78] 5batch rates: [0.72, 0.7, 0.75, 0.69, 0.71, 0.78] 6range: 0.69 to 0.78

A confidence interval from the last chapter uses a finite sample to quantify uncertainty about an unknown rate. A distribution does the opposite: it assumes a process, then generates the samples you might see. The interval asks what the data support; the simulator asks what the assumption would produce.

What is the difference between p = 0.72 and a sampled batch rate of 0.68?

Answer

p = 0.72 is the assumed long-run resolution probability in the simulator. 0.68 is one finite batch generated under that assumption. A different batch can land above or below 0.72.

Binary resolution is only one label. Real traffic is split across several routes.

Categorical: one route per request

An incident-assistant request isn't always binary. It may be exactly one of several route labels. For this planning mix:

IntentProbabilityExpected count among 500 requests
ci failure0.45225
rollback0.20100
access request0.20100
log triage0.1575

The four probabilities sum to 1.00 because every request receives one route in this simplified taxonomy. A categorical distribution draws one of those labels according to the supplied weights.

One request gives one categorical outcome. If 500 requests are independent and use this same route mix, the four counts together follow a multinomial distribution. Their sum must be 500, so the counts aren't independent of one another. This sampler draws requests one at a time, then counts labels.

Predict before running it: the observed counts should be near 225, 100, 100, and 75, but they don't need to match those expected counts exactly. Sampling variation is the behavior we want to inspect.

sample-intent-routing.py
1from random import Random 2 3intents = ["ci_failure", "rollback", "access_request", "log_triage"] 4probabilities = [0.45, 0.20, 0.20, 0.15] 5requests = 500 6 7assert abs(sum(probabilities) - 1.0) < 1e-12 8 9rng = Random(21) 10sampled = rng.choices(intents, weights=probabilities, k=requests) 11expected = { 12 intent: int(requests * probability) 13 for intent, probability in zip(intents, probabilities) 14} 15observed = {intent: sampled.count(intent) for intent in intents} 16 17print("expected counts:", expected) 18print("sampled counts: ", observed) 19print("all labels known:", set(sampled).issubset(set(intents)))
Categorical route sample
1expected counts: {'ci_failure': 225, 'rollback': 100, 'access_request': 100, 'log_triage': 75} 2sampled counts: {'ci_failure': 218, 'rollback': 105, 'access_request': 106, 'log_triage': 71} 3all labels known: True

The sampled counts needn't equal the expectations. That's ordinary variation. An unknown label such as database_restore would be a different problem: the taxonomy or the sampler contract is wrong. choices accepts relative weights, so [45, 20, 20, 15] would encode the same distribution; our explicit probabilities make the planning assumptions easier to inspect.[1]Reference 1random: Generate pseudo-random numbershttps://docs.python.org/3/library/random.html

Routes tell you what kind of request arrived. Tool calls tell you how expensive that request was to serve.

Poisson counts, then a bursty counterexample

Tool calls are counts. A simple baseline is a Poisson distribution with parameter lambda, written λ\lambdaλ. If lambda = 2.2, the simulated agent makes 2.2 tool calls per request on average.

With a mean of 2.2, a Poisson model predicts about 26.8% of requests will make exactly two calls. To calculate that value, let C denote the count and k one nonnegative integer. In the formula below, k! means the factorial (2! = 2 × 1), and e is the base of natural logarithms:

P(C=k)=λke−λk!P(C=k) = \frac{\lambda^k e^{-\lambda}}{k!}P(C=k)=k!λke−λ​

With λ=2.2\lambda = 2.2λ=2.2 and k=2k = 2k=2:

P(C=2)=2.22e−2.22=4.84×e−2.22≈0.268P(C=2) = \frac{2.2^2 e^{-2.2}}{2} = \frac{4.84 \times e^{-2.2}}{2} \approx 0.268P(C=2)=22.22e−2.2​=24.84×e−2.2​≈0.268

The loop below prints a few more masses, including the expensive tail P(C > 5).

poisson-tool-call-probabilities.py
1from math import exp, factorial 2 3lam = 2.2 4 5def poisson_probability(k: int) -> float: 6 if k < 0: 7 raise ValueError("k must be nonnegative") 8 return lam**k * exp(-lam) / factorial(k) 9 10for k in range(5): 11 print(f"P(calls = {k}): {poisson_probability(k):.3f}") 12 13share_above_5 = 1 - sum(poisson_probability(k) for k in range(6)) 14print(f"P(calls > 5): {share_above_5:.3f}") 15 16try: 17 poisson_probability(-1) 18except ValueError as error: 19 print(error)
Poisson count probabilities
1P(calls = 0): 0.111 2P(calls = 1): 0.244 3P(calls = 2): 0.268 4P(calls = 3): 0.197 5P(calls = 4): 0.108 6P(calls > 5): 0.025 7k must be nonnegative

The Poisson model makes a stronger claim than "counts can't be negative." Its mean and variance are both λ\lambdaλ.[4]Reference 4NIST/SEMATECH e-Handbook: Poisson Distributionhttps://www.itl.nist.gov/div898/handbook/eda/section3/eda366j.htm A Poisson process produces such counts in a fixed exposure window when events arrive independently at a constant rate. Tool calls within a request aren't automatically such a process: the agent often decides its next call based on a previous result. Here Poisson is a candidate count distribution, not a fact implied by the data type.

An agent can break that assumption. A difficult rollback may trigger a deploy lookup, a runbook lookup, a retry, and a handoff together. Those bursts create overdispersion, where variance is much larger than the mean.

Both streams in the next fixture have a mean near 2.2. The mixed stream uses ordinary requests 88% of the time and hard requests 12% of the time. Its exact expected count is 0.88 × 1.2 + 0.12 × 10 = 2.256, close to the baseline's 2.2.

Predict what a good Poisson fit would show: a variance-to-mean ratio near 1 and a share above five calls near .025. The transparent product-based sampler below is restricted to small rates for this exercise. For general-purpose simulation, use a tested library sampler such as NumPy's Generator.poisson, not this loop at large rates.

challenge-poisson-tool-calls.py
1from math import exp 2from random import Random 3 4def sample_poisson(rng: Random, lam: float) -> int: 5 if not 0 <= lam <= 20: 6 raise ValueError("this teaching sampler requires 0 <= lambda <= 20") 7 threshold = exp(-lam) 8 count = 0 9 product = 1.0 10 while True: 11 product *= rng.random() 12 if product <= threshold: 13 return count 14 count += 1 15 16def mean(xs: list[float]) -> float: 17 return sum(xs) / len(xs) 18 19def pop_variance(xs: list[float]) -> float: 20 center = mean(xs) 21 return sum((x - center) ** 2 for x in xs) / len(xs) 22 23rng = Random(31) 24steady = [sample_poisson(rng, 2.2) for _ in range(5_000)] 25bursty = [] 26for _ in range(5_000): 27 hard_request = rng.random() < 0.12 28 bursty.append(sample_poisson(rng, 10.0 if hard_request else 1.2)) 29 30def report(name: str, counts: list[int]) -> None: 31 average = mean(counts) 32 variance = pop_variance(counts) 33 above_five = sum(count > 5 for count in counts) / len(counts) 34 print( 35 f"{name:>6}: mean={average:.2f} variance={variance:.2f} " 36 f"variance/mean={variance / average:.2f} share>5={above_five:.3f}" 37 ) 38 39report("steady", steady) 40report("bursty", bursty)
Poisson fit diagnostic
1steady: mean=2.23 variance=2.20 variance/mean=0.98 share>5=0.025 2bursty: mean=2.29 variance=10.66 variance/mean=4.65 share>5=0.115
MetricSteady Poisson (λ=2.2\lambda = 2.2λ=2.2)Bursty Mixture (88% Poisson(1.2)+12% Poisson(10)88\% \text{ Poisson}(1.2) + 12\% \text{ Poisson}(10)88% Poisson(1.2)+12% Poisson(10))Operational Diagnosis
Theoretical Mean2.20 calls2.256 callsAverages appear almost identical
Sample Variance2.2010.66Mixture variance is nearly 5×5\times5× higher
Variance / Mean ratio0.984.65Severe overdispersion signals retry storms
Costly tail (>5> 5>5 calls)2.5%11.5%4.6×4.6\times4.6× higher risk of exhausting tool quotas

The mixed sample has both excess variance and a larger expensive-tail share. Overdispersion alone doesn't determine every tail probability, and a tiny sample can have an unstable variance estimate. Inspect the count frequencies and the actual threshold you care about. Also separate request types or incident waves before deciding whether one distribution should describe them all.

Why isn't a Poisson distribution automatically correct for agent tool calls just because tool calls are counts?

Answer

Poisson is a useful first count model, but it assumes the variance matches the mean. Agent retries and difficult request clusters can create much larger variance and a heavier expensive tail. Check the observed count shape before relying on the baseline.

Counts are only half of the cost story. The other half is how long the request occupies a worker.

Lognormal response time

End-to-end response time can't be negative. Some requests can also take much longer than the typical request, forming a right tail. A lognormal distribution is one candidate for positive, right-skewed values; positivity alone doesn't establish that it's the right family.

A normal distribution is the familiar symmetric bell-shaped model, described by a mean and standard deviation. A variable is lognormal when its natural logarithm is normal. For this example, take the logarithm of the numerical duration expressed in seconds.

random.lognormvariate(mu, sigma) takes the mean and standard deviation of those log values, not of the durations. Set mu = log(6) for a six-second median and sigma = 0.55 for log-space spread. This gives a mean near 6.98 seconds. The conversion rules are:[5]Reference 5NIST/SEMATECH e-Handbook: Lognormal Distributionhttps://www.itl.nist.gov/div898/handbook/eda/section3/eda3669.htm

median(T)=eμE[T]=eμ+σ2/2\text{median}(T) = e^\mu \qquad E[T] = e^{\mu + \sigma^2/2}median(T)=eμE[T]=eμ+σ2/2

To find F_T(10), measure how many log-space standard deviations 10 seconds lies above the center. This standardized distance is a z-score:

z=log⁡10−log⁡60.55=log⁡(10/6)0.55≈0.93z = \frac{\log 10 - \log 6}{0.55} = \frac{\log(10/6)}{0.55} \approx 0.93z=0.55log10−log6​=0.55log(10/6)​≈0.93

The normal CDF with mean zero and standard deviation one gives about 0.823 at this z-score. Thus roughly 82% finish within 10 seconds. Its 95th-percentile z-score is about 1.645, so the model's p95 is exp(log(6) + 0.55 × 1.645) ≈ 14.83 seconds. A finite sample needn't hit either value exactly.

Predict before sampling: the median should stay near 6 seconds, p95 should land near 14.8 seconds, and no draw should be nonpositive. The output below checks those three consequences.

⚠️ Common mistake: Passing 6.0 as mu does not make the median six seconds. It makes the median e6≈403e^{6} \approx 403e6≈403 seconds. NumPy's Generator.lognormal(mean, sigma) uses the same log-space mean.

sample-lognormal-response-time.py
1from math import log 2from random import Random 3from statistics import NormalDist, fmean, median 4 5def percentile(values: list[float], q: float) -> float: 6 ordered = sorted(values) 7 position = (len(ordered) - 1) * q / 100 8 lower = int(position) 9 upper = min(lower + 1, len(ordered) - 1) 10 return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower) 11 12median_seconds = 6.0 13sigma = 0.55 14mu = log(median_seconds) 15 16rng = Random(44) 17response_seconds = [rng.lognormvariate(mu, sigma) for _ in range(5_000)] 18 19print(f"target median seconds: {median_seconds:.2f}") 20print(f"sample median seconds: {median(response_seconds):.2f}") 21print(f"sample mean seconds: {fmean(response_seconds):.2f}") 22print(f"sample p95 seconds: {percentile(response_seconds, 95):.2f}") 23print(f"nonpositive values: {sum(t <= 0 for t in response_seconds)}") 24print(f"model P(T <= 10): {NormalDist(mu, sigma).cdf(log(10)):.3f}")
Positive response-time sample
1target median seconds: 6.00 2sample median seconds: 5.94 3sample mean seconds: 6.94 4sample p95 seconds: 14.80 5nonpositive values: 0 6model P(T <= 10): 0.823

The median describes a typical request. p95 tells you about a noticeably slow slice. Reporting only the mean would hide the operational question customers feel: how long do the slow responses take?

A normal model can go negative

The normal distribution's support extends across the whole real line. It can model signed quantities, such as a measurement error around zero. It can also approximate a positive quantity when the chance of a negative draw is negligible for the task, but that needs checking.

With mean 8 seconds and standard deviation 6 seconds, negative response times are far from negligible. Generate 5,000 durations and check support before accepting the plausible-looking mean or p95:

reject-negative-response-time.py
1from random import Random 2from statistics import fmean 3 4def percentile(values: list[float], q: float) -> float: 5 ordered = sorted(values) 6 position = (len(ordered) - 1) * q / 100 7 lower = int(position) 8 upper = min(lower + 1, len(ordered) - 1) 9 return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower) 10 11rng = Random(44) 12bad_response_seconds = [rng.gauss(8.0, 6.0) for _ in range(5_000)] 13 14print(f"minimum seconds: {min(bad_response_seconds):.2f}") 15print(f"share below zero: {sum(t < 0 for t in bad_response_seconds) / len(bad_response_seconds):.3f}") 16print(f"p95 seconds: {percentile(bad_response_seconds, 95):.2f}") 17print(f"sample mean: {fmean(bad_response_seconds):.2f}") 18 19assert any(t < 0 for t in bad_response_seconds)
Invalid latency model
1minimum seconds: -15.70 2share below zero: 0.095 3p95 seconds: 17.85 4sample mean: 8.09

About 9.5% of this sample is physically impossible. Clipping every negative draw to zero would create a pile of zero-duration requests and change the distribution, not establish a fit. A positive family, a justified truncated model, or sampling representative empirical durations are alternatives to evaluate against actual traces.

Even a plausible distribution for isolated durations misses another source of latency: waiting behind other requests. First distinguish time spent doing work from time spent in a queue.

Exponential service time

An exponential distribution models a duration with a constant completion rate, conditional on not having finished yet. It's the waiting-time distribution between arrivals in a constant-rate Poisson process. Using it for service time is a separate modeling assumption.

For a toy single-server service, let S be service time and μ = 20 completions per second when the server is busy. The mean duration is 1 / 20 = 0.05 seconds. This 50 ms service is a separate queueing example, not a new parameter for the six-second agent response-time model.

Its density and CDF are:

fS(s)=μe−μs,FS(s)=P(S≤s)=1−e−μs,s≥0f_S(s) = \mu e^{-\mu s}, \qquad F_S(s) = P(S \leq s) = 1 - e^{-\mu s}, \qquad s \geq 0fS​(s)=μe−μs,FS​(s)=P(S≤s)=1−e−μs,s≥0

At 20 requests per second, P(S > 0.10) = e^{-20 \times 0.10} \approx 0.135. The distribution is memoryless: after a request has already taken 50 ms, its remaining service-time distribution is still the same as at the start. That makes it a useful queueing baseline, not a claim that real tool work forgets its elapsed time.[2]Reference 2Machine Learning: A Probabilistic Perspective.https://probml.github.io/pml-book/book0.html[6]Reference 6Pattern Recognition and Machine Learning.https://www.microsoft.com/en-us/research/publication/pattern-recognition-machine-learning/

Before running the sampler, predict a mean near .05 seconds and a share near .135 above .10 seconds. Random.expovariate(mu) takes the rate μ; NumPy's Generator.exponential takes the mean duration as scale = 1 / μ. Passing 20 as the scale instead of 0.05 makes the mean 400 times larger. Although exact zero has probability zero in the mathematical model, a finite-precision sampler can return it, so the support check allows zero.[1]Reference 1random: Generate pseudo-random numbershttps://docs.python.org/3/library/random.html

sample-exponential-service-time.py
1from math import exp 2from random import Random 3from statistics import fmean 4 5rate_per_second = 20.0 6scale_seconds = 1 / rate_per_second 7rng = Random(66) 8service_seconds = [rng.expovariate(rate_per_second) for _ in range(5_000)] 9 10print(f"target mean seconds: {scale_seconds:.3f}") 11print(f"sample mean seconds: {fmean(service_seconds):.3f}") 12print(f"P(S > 0.10) formula: {exp(-rate_per_second * 0.10):.3f}") 13print(f"sample share > 0.10: {sum(t > 0.10 for t in service_seconds) / len(service_seconds):.3f}") 14print(f"min seconds: {min(service_seconds):.6f}") 15 16assert all(t >= 0 for t in service_seconds)
Exponential service-time baseline
1target mean seconds: 0.050 2sample mean seconds: 0.049 3P(S > 0.10) formula: 0.135 4sample share > 0.10: 0.132 5min seconds: 0.000025

The sample follows the chosen model. It doesn't show that real service times are exponential. Repeated simulation can estimate a model's tail probability more precisely without improving the model's fit to reality.

More draws reduce simulation noise

Monte Carlo simulation means using repeated random samples to approximate a quantity you care about. Here the quantity is the resolution rate, estimated from repeated simulated batches.

For N = 100 independent Bernoulli requests with p = 0.72, the batch-rate standard deviation is sqrt(0.72 × 0.28 / 100) ≈ 0.045. With N = 10,000, it's about 0.0045. This is the earlier standard-error formula: sqrt(p(1-p)/N). Multiplying the sample size by 100 divides this uncertainty by 10, not by 100.

Predict the three standard deviations before running the cell: they should be roughly .045, .014, and .0045. That pattern is convergence under the assumption, not evidence that the assumption came from production.

measure-monte-carlo-wobble.py
1from random import Random 2 3def mean(xs: list[float]) -> float: 4 return sum(xs) / len(xs) 5 6def pop_std(xs: list[float]) -> float: 7 center = mean(xs) 8 return (sum((x - center) ** 2 for x in xs) / len(xs)) ** 0.5 9 10rng = Random(55) 11p_resolved = 0.72 12 13for batch_size in [100, 1_000, 10_000]: 14 repeated_rates = [ 15 rng.binomialvariate(batch_size, p_resolved) / batch_size 16 for _ in range(2_000) 17 ] 18 print( 19 f"N={batch_size:>5}: mean={mean(repeated_rates):.3f} " 20 f"sd_of_rates={pop_std(repeated_rates):.4f}" 21 )
Monte Carlo convergence
1N= 100: mean=0.720 sd_of_rates=0.0457 2N= 1000: mean=0.720 sd_of_rates=0.0140 3N=10000: mean=0.720 sd_of_rates=0.0045

Simulation answers a conditional question: "What happens if these assumptions are the generating process?" It doesn't validate p = 0.72, lambda = 2.2, or the latency parameters. Those must come from data, review design, and production measurement.

To investigate a surprising run, record enough information to replay it rather than hoping the next random draw looks better.

Recorded seeds replay a stream

Random(seed) creates a deterministic pseudo-random stream. In the same environment, reusing the seed and draw order recreates a run. Record the code, Python and library versions, parameters, and number of draws too. The seed alone isn't a promise that every distribution sampler produces identical results across upgrades.[1]Reference 1random: Generate pseudo-random numbershttps://docs.python.org/3/library/random.html

A seed doesn't make observations independent, and it doesn't repair a wrong model. Independence is a modeling assumption; separate streams help avoid accidentally replaying the same sequence across replicas.

For parallel replications, create separate Random objects and record each stream ID. The snippet derives two IDs from one root label. NumPy's Generator.spawn offers a designed child-stream mechanism for parallel work; distinct streams avoid replaying identical pseudo-observations as if they were new evidence.[7]Reference 7Parallel random number generationhttps://numpy.org/doc/stable/reference/random/parallel.html

replay-replica-rng-streams.py
1from random import Random 2 3root = "incident-sim-2026" 4replica_one = Random(f"{root}:replica-1") 5replica_two = Random(f"{root}:replica-2") 6counts_one = [replica_one.binomialvariate(20, 0.72) for _ in range(4)] 7counts_two = [replica_two.binomialvariate(20, 0.72) for _ in range(4)] 8 9replay_one = Random(f"{root}:replica-1") 10replay_two = Random(f"{root}:replica-2") 11replayed_one = [replay_one.binomialvariate(20, 0.72) for _ in range(4)] 12replayed_two = [replay_two.binomialvariate(20, 0.72) for _ in range(4)] 13 14print("replica 1:", counts_one) 15print("replica 2:", counts_two) 16print("replay matches:", counts_one == replayed_one and counts_two == replayed_two) 17 18assert counts_one == replayed_one 19assert counts_two == replayed_two
Reproducible replica streams
1replica 1: [16, 14, 16, 12] 2replica 2: [14, 16, 13, 17] 3replay matches: True

Replay is a reporting contract. It also helps inspect a more difficult sampling problem: a Bayesian posterior that can't be sampled directly.

Sample a posterior when direct draws aren't available

Bayesian Inference from Evidence updated a Beta(2, 2) prior with eight passes and two failures to obtain an exact Beta(10, 4) posterior. That direct update worked because the prior and likelihood were conjugate. A richer agent model might include several interacting skill parameters or task-difficulty effects without a convenient closed-form posterior.

Markov chain Monte Carlo (MCMC) addresses that harder case by constructing a sequence of dependent parameter states whose long-run distribution matches the desired posterior. Unlike the independent Monte Carlo draws above, each new state starts from the previous state.[2]Reference 2Machine Learning: A Probabilistic Perspective.https://probml.github.io/pml-book/book0.html

Metropolis-Hastings proposes a candidate state, then either moves to it or stays at the current state. Start with a concrete comparison: the Beta(10, 4) posterior's unnormalized density is about 2.642 times as high at 0.6 as at 0.5. With a symmetric proposal, accept the move to 0.6 with probability 1; accept the reverse move with probability 1 / 2.642 ≈ 0.379.

Here symmetric means the proposal is equally likely in either direction, as with a normal step centered on the current value. If π\piπ denotes the target density and p′p'p′ a proposed value, this rule is:[8]Reference 8Monte Carlo Sampling Methods Using Markov Chains and Their Applicationshttps://www2.stat.duke.edu/~sschmid/Courses/Stat376/Papers/Basic/Hastings1970.pdf

a(p,p′)=min⁡(1,π(p′)π(p))a(p,p')=\min\left(1,\frac{\pi(p')}{\pi(p)}\right)a(p,p′)=min(1,π(p)π(p′)​)

The unknown normalization constant in π\piπ cancels in the ratio. The earlier Beta(10, 4) result lets us check the mechanics even though this posterior can be sampled directly with betavariate. Its density is proportional to p9(1−p)3p^9(1-p)^3p9(1−p)3 for 0 < p < 1. An asymmetric proposal needs an additional reverse-to-forward proposal-density ratio; don't reuse the simplified rule unchanged.

The next cell checks both directions with a fixed comparison value of 0.4. Predict why that value accepts the forward move but rejects the reverse move. This is one deterministic acceptance check; a complete sampler must draw a fresh comparison value at every iteration.

inspect-metropolis-hastings-acceptance.py
1def unnormalized_posterior(probability: float) -> float: 2 return probability**9 * (1 - probability) ** 3 3 4forward_ratio = unnormalized_posterior(0.6) / unnormalized_posterior(0.5) 5reverse_ratio = 1 / forward_ratio 6comparison = 0.4 7 8print(f"forward acceptance: {min(1.0, forward_ratio):.3f}") 9print(f"reverse acceptance: {min(1.0, reverse_ratio):.3f}") 10print(f"reverse accepted at u=0.4: {comparison < reverse_ratio}")
Metropolis-Hastings acceptance check
1forward acceptance: 1.000 2reverse acceptance: 0.379 3reverse accepted at u=0.4: False

A rejection must remain in the chain as another copy of the current state. Keeping only accepted moves would sample a different process. This complete teaching sampler uses log densities to avoid underflow, rejects proposals outside [0, 1], and compares four starting points with the known posterior mean 10 / 14:

sample-beta-posterior-with-metropolis.py
1from math import log, log1p 2from random import Random 3from statistics import fmean 4 5def log_target(p): 6 if not 0 < p < 1: 7 return float("-inf") 8 return 9 * log(p) + 3 * log1p(-p) 9 10def sample_chain(seed, start, warmup=2_000, kept=10_000): 11 rng = Random(seed) 12 current = start 13 draws = [] 14 accepted = 0 15 for step in range(warmup + kept): 16 proposal = current + rng.gauss(0, 0.15) 17 log_ratio = log_target(proposal) - log_target(current) 18 # 1-random() lies in (0, 1], so log is defined. 19 if log(1 - rng.random()) < min(0.0, log_ratio): 20 current = proposal 21 if step >= warmup: 22 accepted += 1 23 if step >= warmup: 24 draws.append(current) # include repeats after rejection 25 return draws, accepted / kept 26 27for seed, start in enumerate([0.1, 0.4, 0.7, 0.95], start=101): 28 draws, acceptance = sample_chain(seed, start) 29 center = fmean(draws) 30 variance_sum = sum((p - center) ** 2 for p in draws) 31 lag_one = sum((a - center) * (b - center) 32 for a, b in zip(draws[:-1], draws[1:])) / variance_sum 33 print(f"start={start:.2f} mean={center:.3f} " 34 f"acceptance={acceptance:.3f} lag1={lag_one:.3f}") 35 assert abs(center - 10 / 14) < 0.02 # a loose known-target smoke check 36print(f"exact posterior mean: {10 / 14:.3f}")
Posterior chain checks
1start=0.10 mean=0.708 acceptance=0.644 lag1=0.715 2start=0.40 mean=0.712 acceptance=0.637 lag1=0.720 3start=0.70 mean=0.716 acceptance=0.637 lag1=0.720 4start=0.95 mean=0.711 acceptance=0.632 lag1=0.726 5exact posterior mean: 0.714

Early warmup or burn-in states can reflect initialization rather than the posterior. Discarding them doesn't guarantee that later states explore well. The printed lag-one correlation measures similarity between adjacent draws; it can stay large even when the sample mean looks right.

Autocorrelation reduces the information in many ordinary chains. Effective sample size estimates precision for a particular quantity relative to independent draws; chain length alone doesn't determine it. Established tools report rank-normalized split R-hat, which compares variation within and between chains, alongside effective sample sizes. Passing the loose mean assertion above is a code smoke check against a known answer, not a convergence proof.[9]Reference 9Posterior Analysishttps://mc-stan.org/docs/reference-manual/analysis.html

Gibbs sampling instead updates one variable from its conditional distribution given the current values of the others. Exact conditional draws can still produce a strongly correlated chain. Neither method establishes that the underlying statistical model represents the real system.

Posterior-sampling diagnostics qualify uncertainty about model parameters. They still say nothing about queues, which is where isolated latency samples go wrong.

Waiting time when the server fills up

A simulation that samples latency in isolation ignores how requests interact. Suppose three requests arrive at 0, 10, and 20 ms, and each needs 50 ms on one server. The first starts immediately. The second waits until 50 ms, and the third until 100 ms. Their total times are therefore 50, 90, and 130 ms, even though each does the same amount of work.

That is queueing delay: time waiting to start, separate from service time. A probabilistic queueing model adds random arrivals and service durations to this same mechanism.

Little's Law states that the average number of requests in a stable system LLL equals the arrival rate λ\lambdaλ times the average time WWW a request spends in the system:

L=λWL = \lambda WL=λW

For an M/M/1 baseline, assume constant-rate Poisson arrivals, independent exponential service times, one server, first-come-first-served processing, unlimited waiting space, and no abandonment. The two Ms denote the memoryless arrival and service models. At a steady arrival rate of 15 requests per second and a service rate of 20 per second, the server is busy on average 15 / 20 = 75% of the time.

Write utilization as ρ=λ/μ\rho = \lambda / \muρ=λ/μ. A steady-state distribution requires λ<μ\lambda < \muλ<μ. Here λ\lambdaλ counts arrivals per second; the earlier tool-count parameter 2.2 counted calls per request. The same Greek letter doesn't imply the same units.[10]Reference 10Introduction to Queueing Theory: 6.263/16.37, Lectures 5 and 6https://web.mit.edu/modiano/www/6.263/lec5-6.pdf

Average total time in the system WWW and average time waiting in the queue WqW_qWq​ are:

W=1μ−λWq=λμ(μ−λ)W = \frac{1}{\mu - \lambda} \qquad W_q = \frac{\lambda}{\mu(\mu - \lambda)}W=μ−λ1​Wq​=μ(μ−λ)λ​

As utilization approaches 100%, both quantities rise sharply. Consider a service with a mean service time of 50 ms (μ=20\mu = 20μ=20 requests/second):

Arrivals λ\lambdaλUtilization ρ\rhoρTotal time WWWQueue time WqW_qWq​
15 req/s0.750.20 s0.15 s
19 req/s0.951.00 s0.95 s

The service time stays at 50 ms in both rows, but moving from 75% to 95% utilization lifts average total time from 0.20 to 1.00 seconds. Queueing, not slower work, caused the jump.

These are steady-state averages, not predictions for a three-request burst or a p99 latency. At or above λ = μ, this model has no finite steady-state mean waiting time; plugging those values into the formula is invalid. Little's Law alone doesn't cause the sharp increase: that shape comes from the M/M/1 assumptions. Real batching, parallel workers, deadlines, and dependent retries need a different queueing model.

Two ways a latency simulator can lie are already on the table: the wrong positive-time family, and a missing queue. The last diagnostic uses the CDF you already have.

Score a candidate with the CDF

Before using a simulator for planning, compare it with representative held-out traces, including difficult requests and timeouts. A quantile-quantile (Q-Q) plot compares observed quantiles against candidate quantiles; disagreement near the high end exposes a tail mismatch. A timeout recorded at its deadline is a censored observation, not necessarily a completed response at exactly that duration.

The empirical CDF at a deadline is simply the fraction of recorded durations at or below it. If 160 of 200 completed responses finish within 10 seconds, it equals 0.80 there. A candidate CDF of 0.823 has a gap of 0.023 at that deadline.

The Kolmogorov-Smirnov (KS) distance DDD takes the largest such vertical gap over all deadlines. In the notation below, sup means the largest gap, F_n is the empirical CDF, and F is the candidate CDF:

D=sup⁡t∣Fn(t)−F(t)∣D = \sup_t \left| F_n(t) - F(t) \right|D=tsup​∣Fn​(t)−F(t)∣

On the same sample, a larger D means a larger worst-case CDF discrepancy. It isn't a verdict on every operational risk: two models can differ mainly in a rare tail that contributes little to D. Keep your explicit timeout and costly-tail checks.

The ordinary one-sample KS p-value assumes independent observations from a continuous distribution whose parameters were specified in advance. Fitting parameters on the same observations needs a calibration procedure that accounts for fitting, such as a parametric bootstrap. SciPy's goodness_of_fit implements this approach.[11]Reference 11scipy.stats.goodness_of_fithttps://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.goodness_of_fit.html This example only calculates distances, not p-values.

This check specifies both candidates first, using the same lognormal planning values as the rest of the chapter, then scores them against 200 positive draws. The normal candidate gets the lognormal's mean and standard deviation, so the comparison tests shape beyond those summaries. We expect the generating lognormal model to fit better on average, though a finite sample doesn't guarantee that ordering.

ks-distance-challenge.py
1from math import erf, exp, log, sqrt 2from random import Random 3 4def normal_cdf(x: float, mu: float, sigma: float) -> float: 5 z = (x - mu) / sigma 6 return 0.5 * (1.0 + erf(z / sqrt(2.0))) 7 8def lognormal_cdf(t: float, mu: float, sigma: float) -> float: 9 if t <= 0: 10 return 0.0 11 return normal_cdf(log(t), mu, sigma) 12 13def ks_distance(samples: list[float], cdf) -> float: 14 if not samples: 15 raise ValueError("KS distance needs at least one observation") 16 ordered = sorted(samples) 17 n = len(ordered) 18 distance = 0.0 19 for index, value in enumerate(ordered, start=1): 20 fitted = cdf(value) 21 distance = max( 22 distance, 23 abs(index / n - fitted), 24 abs((index - 1) / n - fitted), 25 ) 26 return distance 27 28mu = log(6.0) 29sigma = 0.55 30rng = Random(88) 31observed_traces = [rng.lognormvariate(mu, sigma) for _ in range(200)] 32 33normal_mean = exp(mu + sigma**2 / 2) 34normal_std = sqrt((exp(sigma**2) - 1) * exp(2 * mu + sigma**2)) 35 36d_norm = ks_distance( 37 observed_traces, 38 lambda x: normal_cdf(x, normal_mean, normal_std), 39) 40d_log = ks_distance( 41 observed_traces, 42 lambda t: lognormal_cdf(t, mu, sigma), 43) 44 45print(f"normal D: {d_norm:.3f}") 46print(f"lognormal D: {d_log:.3f}") 47print(f"normal mean: {normal_mean:.2f}s std: {normal_std:.2f}s")
KS distance challenge
1normal D: 0.135 2lognormal D: 0.056 3normal mean: 6.98s std: 4.15s

The lognormal gap is smaller in this seeded run. A finite sample still doesn't give D = 0. This is a check that our comparison can distinguish the known generating shape from an alternative, not validation against actual service traces.

Build a launch-simulation report

Now put the four request fields together. This is deliberately an independent-field baseline: it draws each field without conditioning on the others. It has no arrivals, worker limits, or queue, so it describes synthetic requests, not system capacity. Keep that limitation attached to the report.

build-agent-launch-report.py
1from math import exp, log 2from random import Random 3from statistics import fmean, median 4 5def sample_poisson(rng: Random, lam: float) -> int: 6 if not 0 <= lam <= 20: 7 raise ValueError("this teaching sampler requires 0 <= lambda <= 20") 8 threshold = exp(-lam) 9 count = 0 10 product = 1.0 11 while True: 12 product *= rng.random() 13 if product <= threshold: 14 return count 15 count += 1 16 17def percentile(values: list[float], q: float) -> float: 18 ordered = sorted(values) 19 position = (len(ordered) - 1) * q / 100 20 lower = int(position) 21 upper = min(lower + 1, len(ordered) - 1) 22 return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower) 23 24def simulate_incident_agent(seed: int, n_requests: int) -> dict[str, float]: 25 if n_requests <= 0: 26 raise ValueError("n_requests must be positive") 27 28 rng = Random(seed) 29 resolved = [rng.binomialvariate(1, 0.72) for _ in range(n_requests)] 30 31 intents = ["ci_failure", "rollback", "access_request", "log_triage"] 32 intent_probs = [0.45, 0.20, 0.20, 0.15] 33 routed = rng.choices(intents, weights=intent_probs, k=n_requests) 34 35 tool_calls = [sample_poisson(rng, 2.2) for _ in range(n_requests)] 36 response_seconds = [ 37 rng.lognormvariate(log(6.0), 0.55) for _ in range(n_requests) 38 ] 39 40 return { 41 "resolution_rate": fmean(resolved), 42 "rollback_share": routed.count("rollback") / n_requests, 43 "mean_tool_calls": fmean(tool_calls), 44 "share_over_5_tools": sum(count > 5 for count in tool_calls) / n_requests, 45 "median_seconds": median(response_seconds), 46 "p95_seconds": percentile(response_seconds, 95), 47 "nonpositive_times": float(sum(t <= 0 for t in response_seconds)), 48 } 49 50report = simulate_incident_agent(seed=7, n_requests=5_000) 51for name, value in report.items(): 52 print(f"{name:>20}: {value:.3f}") 53 54assert report["nonpositive_times"] == 0 55assert report["p95_seconds"] > report["median_seconds"]
Agent launch simulation report
1resolution_rate: 0.718 2 rollback_share: 0.202 3 mean_tool_calls: 2.206 4 share_over_5_tools: 0.024 5 median_seconds: 6.070 6 p95_seconds: 15.688 7 nonpositive_times: 0.000

Use that output as a simulator report, not a production claim. The rate, label mix, count tail, and latency tail are only as credible as their fitted parameters and assumption checks.

This fixture samples each field separately from one generator. It therefore treats resolution, route, tool count, and response time as independent once their parameters are fixed.

A shared random-number generator (RNG) makes the run replayable; it doesn't create or prove independence. Real rollback requests may use more tools and take longer. Preserve that relationship with conditional sampling, such as drawing route first, then drawing tool count and latency from models for that route.

To see what separate summaries miss, compare two four-request datasets. They have identical tool counts and identical durations, but pair those values differently. Predict how often a request is both expensive and slow:

check-joint-expensive-and-slow.py
1tools = [1, 1, 6, 6] 2aligned_seconds = [2, 2, 20, 20] 3reversed_seconds = [20, 20, 2, 2] 4 5def joint_share(counts, seconds): 6 return sum(c > 5 and t > 15 for c, t in zip(counts, seconds, strict=True)) / len(counts) 7 8assert sorted(aligned_seconds) == sorted(reversed_seconds) 9print(f"aligned expensive-and-slow: {joint_share(tools, aligned_seconds):.2f}") 10print(f"reversed expensive-and-slow: {joint_share(tools, reversed_seconds):.2f}")
Same marginals, different joint risk
1aligned expensive-and-slow: 0.50 2reversed expensive-and-slow: 0.00

Every one-field histogram, mean, and percentile is identical between these datasets. Yet the joint threshold shares differ. Matching each marginal distribution separately doesn't validate the relationships among fields, which matter for timeouts and simultaneous resource demand.

Those checks apply to the simulator's fields. The same categorical math appears inside the model that writes the assistant's answer.

Next tokens: categorical draws, temperature scaling, and the Gumbel-Max trick

The incident-assistant labels form a categorical distribution because one label is selected from a finite list. An LLM uses the same family over a much larger vocabulary. At each step it produces a logit, an unnormalized score, for each candidate token. Softmax exponentiates those scores and divides by their sum, producing probabilities. A decoder may sample one token from that distribution.[12]Reference 12Deep Learning.https://www.deeplearningbook.org/

Take a tiny continuation for the prompt Deploy is. Four tokens are enough to see the mechanics. approved has the largest probability, but a sample can still choose pending; predict that distinction before running the cell.

sample-one-token-from-logits.py
1from math import exp 2from random import Random 3 4tokens = ["approved", "pending", "blocked", "coupon"] 5logits = [2.2, 1.5, 0.2, -1.5] 6 7def stable_softmax(values: list[float]) -> list[float]: 8 shifted = [value - max(values) for value in values] 9 weights = [exp(value) for value in shifted] 10 total = sum(weights) 11 return [weight / total for weight in weights] 12 13probabilities = stable_softmax(logits) 14rng = Random(5) 15chosen = rng.choices(tokens, weights=probabilities, k=1)[0] 16 17for token, probability in zip(tokens, probabilities): 18 print(f"{token:>8}: {probability:.3f}") 19print("sampled token:", chosen) 20print("probabilities sum to one:", abs(sum(probabilities) - 1.0) < 1e-12)
Tiny token distribution
1approved: 0.604 2 pending: 0.300 3 blocked: 0.082 4 coupon: 0.015 5sampled token: pending 6probabilities sum to one: True

approved is the mode, the most probable outcome, but this draw returned pending. Greedy decoding would choose approved; sampling can select any token with nonzero probability. Subtracting the largest logit before exponentiating prevents overflow for these finite inputs without changing the probability ratios. Displayed rounded probabilities may add to 1.001; the unrounded values still sum to one.

Reshaping entropy with temperature scaling

Before feeding logits into softmax, generation runtimes introduce a positive temperature parameter T>0T > 0T>0 that scales logits: z~i=zi/T\tilde{z}_i = z_i / Tz~i​=zi​/T.

Dividing logits by temperature alters the distribution's entropy without changing which token has the highest score:

  • Low temperature (T<1.0T < 1.0T<1.0, such as T=0.3T = 0.3T=0.3): Logit gaps stretch wider. Dividing by 0.3 multiplies differences by 3.33×3.33\times3.33×. Exponentiating those stretched gaps concentrates almost all mass on the top token (approved climbs from 60.4% to 91.1%), squashing competitor probabilities. As T→0T \to 0T→0, categorical sampling converges to deterministic greedy argmax.
  • Baseline temperature (T=1.0T = 1.0T=1.0): Softmax evaluates raw logits directly, preserving the model's calibrated uncertainty.
  • High temperature (T>1.0T > 1.0T>1.0, such as T=2.0T = 2.0T=2.0): Logit gaps shrink toward zero. The probabilities flatten toward a uniform distribution (1/4=25%1/4 = 25\%1/4=25%), making generation exploratory and diverse, but increasing the risk of low-probability hallucinated tokens like coupon.

GPU-parallel sampling with the Gumbel-Max trick

On GPU hardware serving thousands of concurrent requests, how does code draw a categorical sample from 128,000 candidate tokens?

Computing a cumulative probability array (CDF) and performing sequential binary search requires expensive prefix sums and branches that cause GPU thread divergence. Instead, production runtimes use the Gumbel-Max trick.

Draw an independent standard Gumbel noise value for each vocabulary token from uniform random numbers:

gi=−ln⁡(−ln⁡(ui)),ui∼Uniform(0,1)g_i = -\ln(-\ln(u_i)), \qquad u_i \sim \text{Uniform}(0, 1)gi​=−ln(−ln(ui​)),ui​∼Uniform(0,1)

Add that Gumbel noise directly to the temperature-scaled logits, then take the parallel argmax:

i∗=argmax⁡i(ziT+gi)i^* = \operatorname{argmax}_i \left( \frac{z_i}{T} + g_i \right)i∗=argmaxi​(Tzi​​+gi​)

A celebrated result in extreme value theory proves that the probability of token kkk achieving the highest perturbed score equals its softmax probability exactly:

P(argmax⁡i(ziT+gi)=k)=exp⁡(zk/T)∑jexp⁡(zj/T)P\left(\operatorname{argmax}_i \left( \frac{z_i}{T} + g_i \right) = k\right) = \frac{\exp(z_k / T)}{\sum_j \exp(z_j / T)}P(argmaxi​(Tzi​​+gi​)=k)=∑j​exp(zj​/T)exp(zk​/T)​

This reparameterization turns sampling into a parallel vector addition and an argmax reduction, running at native GPU memory bandwidth.

Categorical token sampling dynamics. Top row contrasts temperature scaling across three regimes for candidate tokens approved, pending, blocked, and coupon: T=0.3 concentrates 90 percent mass on approved (sharp/greedy); T=1.0 maintains balanced probabilities (60.4 percent approved, 30.0 percent pending); T=2.0 flattens the distribution (diffuse/exploratory). Bottom panel illustrates Gumbel-Max reparameterization: adding independent standard Gumbel perturbations g = -log(-log u) to scaled logits enables GPUs to draw exact categorical samples in parallel using deterministic argmax.
Top: temperature scales logits before softmax, trading mode sharpness (T=0.3) against uniform entropy (T=2.0). Bottom: the Gumbel-Max trick adds independent standard Gumbel noise to logits and computes an argmax, turning GPU-friendly parallel vector operations into an exact categorical sample.

In the bottom panel, notice how token pending had a lower base logit (1.50) than approved (2.20). But because pending drew a lucky Gumbel perturbation (+2.25 versus +0.77), its perturbed score reached 3.75, winning the argmax. Across thousands of draws, each token wins with an empirical frequency that mirrors its softmax probability.

The next snippet verifies that equivalence across 10,000 Gumbel-Max draws at both baseline and sharp temperatures:

gumbel-max-token-sampling.py
1from math import exp, log 2from random import Random 3 4def stable_softmax(logits: list[float], temperature: float = 1.0) -> list[float]: 5 scaled = [z / temperature for z in logits] 6 max_z = max(scaled) 7 weights = [exp(z - max_z) for z in scaled] 8 total = sum(weights) 9 return [w / total for w in weights] 10 11def gumbel_sample(rng: Random, logits: list[float], temperature: float = 1.0) -> int: 12 # Standard Gumbel noise: g = -log(-log(u)) where u ~ Uniform(0, 1) 13 perturbed = [] 14 for z in logits: 15 u = 1.0 - rng.random() # strictly in (0, 1] 16 g = -log(-log(u)) 17 perturbed.append(z / temperature + g) 18 return max(range(len(logits)), key=lambda i: perturbed[i]) 19 20tokens = ["approved", "pending", "blocked", "coupon"] 21logits = [2.2, 1.5, 0.2, -1.5] 22 23rng = Random(42) 24n_draws = 10_000 25 26for temp in [1.0, 0.3]: 27 expected_probs = stable_softmax(logits, temperature=temp) 28 samples = [gumbel_sample(rng, logits, temperature=temp) for _ in range(n_draws)] 29 observed_shares = [samples.count(i) / n_draws for i in range(len(tokens))] 30 31 print(f"Temperature T={temp:.1f}:") 32 for token, p_exp, p_obs in zip(tokens, expected_probs, observed_shares): 33 print(f" {token:>8}: softmax={p_exp:.3f} gumbel_share={p_obs:.3f}")
Gumbel-Max sampling verification
1Temperature T=1.0: 2 approved: softmax=0.604 gumbel_share=0.610 3 pending: softmax=0.300 gumbel_share=0.292 4 blocked: softmax=0.082 gumbel_share=0.084 5 coupon: softmax=0.015 gumbel_share=0.014 6Temperature T=0.3: 7 approved: softmax=0.911 gumbel_share=0.902 8 pending: softmax=0.088 gumbel_share=0.097 9 blocked: softmax=0.001 gumbel_share=0.001 10 coupon: softmax=0.000 gumbel_share=0.000

Later chapters teach decoding controls in depth. A sampling decoder doesn't choose from raw logits directly. It draws from a probability distribution derived from those logits. A greedy decoder is different: it selects the highest-probability token instead of drawing a sample. For open-ended text generation, nucleus sampling chooses a dynamic high-probability token set and renormalizes it before sampling, rather than sampling from an unreliable long tail.[13]Reference 13The Curious Case of Neural Text Degeneration.https://arxiv.org/abs/1904.09751

Keep the simulation honest

Before believing output from a random simulator, write down its contract:

QuantityModel usedReportChallenge before trusting it
resolved without humanBernoullirateis one request one binary decision?
request routecategoricalproportionsare all real route labels represented?
tool callsPoisson baselinemean and share above 5is variance near mean, or are calls bursty?
response timelognormal baselinemedian and p95is any time nonpositive, and does the tail resemble traces?
next tokencategorical from softmaxsampled token and probabilitiesdoes decoding alter the candidate set or distribution?

The seed also belongs in the report. It makes one simulated run reproducible, but it doesn't make an incorrect model correct.

Practice: challenge a candidate launch model

A teammate proposes this incident-assistant simulator:

QuantityProposed modelClaim
resolutionBernoulli with p = 0.7878 percent resolves automatically
request typecategorical with no log_triage labelcovers incoming requests
tool callsPoisson with mean 2.0estimates tool cost
response timenormal with mean 7 seconds and standard deviation 8 secondsestimates p95 latency

Write a review with four parts:

  1. Name one parameter that needs evidence from logs or reviewed traffic.
  2. Identify the impossible or missing state.
  3. State one count diagnostic for the tool-call assumption.
  4. State which latency summary should appear in the report.

What is a strong answer to the practice review?

Answer

The resolution probability needs evidence from a representative set of requests. The categorical model is missing log_triage, so it can't represent every stated route. Tool-call counts should be checked for overdispersion by comparing variance with mean and reporting the share above a costly threshold. A normal response-time model can create negative seconds when spread is large; use a positive baseline and report median plus p95.

Before trusting a simulator, build a launch-model worksheet from one seeded run. Record each assumed parameter, check labels and support, compare count variance with the mean, and report median, p95, and costly-tail rates. Use the worksheet to reject or revise assumptions before simulated capacity or cost numbers reach a design review.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A Bernoulli simulator encodes R = 1 for resolved and R = 0 for escalated with p = 0.72. One 100-request run resolves 68 requests. Which statement is correct?

Correct answer: The assumed mean is 0.72; the observed 0.68 is sampling variation within that model.

For a 0/1 Bernoulli variable, the expected value equals the success probability, so E[R] = 0.72. A finite batch rate is a random outcome generated under the assumption, not a requirement that every batch equal the parameter and not proof that the parameter is correct for production.

2.A route sampler is configured with intents ci_failure, rollback, access_request, and log_triage with probabilities 0.45, 0.20, 0.20, and 0.15. In a 500-request sample, one route is database_restore. What should you conclude?

Correct answer: Inspect the sampler or data path: an output lies outside the declared category set.

A categorical sampler may produce counts that differ from their expected counts, but it should only draw from its declared finite label set. Here the probabilities already sum to 1.00, so the problem is not normalization; the unknown label means the taxonomy or sampler contract is wrong.

3.A count PMF reports P(C=2)=0.268. A continuous latency CDF reports F_T(10)=0.823. To sample continuous durations from this CDF, code inverts uniform draws via T=FT−1(U)T = F_T^{-1}(U)T=FT−1​(U). What does this inverse CDF transform guarantee?

Correct answer: The transformed values TTT follow the target continuous distribution exactly, mapping uniform numbers on (0,1)(0, 1)(0,1) into durations where steep CDF regions receive high sample density.

By the probability integral transform, if U∼Uniform(0,1)U \sim \text{Uniform}(0, 1)U∼Uniform(0,1) and FFF is a continuous CDF, the random variable T=F−1(U)T = F^{-1}(U)T=F−1(U) follows FFF exactly: P(F−1(U)≤t)=P(U≤F(t))=F(t)P(F^{-1}(U) \leq t) = P(U \leq F(t)) = F(t)P(F−1(U)≤t)=P(U≤F(t))=F(t). Where the CDF is steep (near the median), a wide swath of uniform draws projects into a tight duration window, generating high sample density.

4.Across several representative batches of 5,000 requests, tool counts have mean near 2.1 and variance near 14.0. What should you do with a Poisson baseline?

Correct answer: Investigate request mixtures and retries; compare both count frequencies and the costly-tail share.

Poisson ties mean and variance to one parameter. A persistent ratio near 14/2.1=6.7 challenges that model, but doesn't uniquely identify a cause or replacement. Changing lambda alone can't match both summaries.

5.A simulator should give response time a six-second median using Random.lognormvariate with sigma = 0.55. Which call matches that goal?

Correct answer: lognormvariate(log(6.0), 0.55): exponentiating the log-space center gives the median.

For a lognormal variable, the normal distribution is applied to log(T), not to seconds directly. Both Python's lognormvariate(mu, sigma) and NumPy's lognormal(mean, sigma) take that log-space center, so mu = log(6) gives a six-second median.

6.You fit lognormal parameters to 200 latency rows, then pass that fitted CDF and the same rows to an ordinary one-sample KS test. Which review comment is justified?

Correct answer: The distance is usable descriptively, but the p-value needs calibration that accounts for fitting on those rows.

The ordinary fixed-CDF calibration doesn't account for selecting parameters to fit this sample. A parametric bootstrap can refit parameters inside each replication. Regardless of the p-value, inspect operational tails and dependence separately.

7.A Bernoulli simulator with assumed p = 0.72 uses a recorded seed and 2,000 repeated batches at N = 100 and N = 10,000. Which conclusion is justified?

Correct answer: The mean stays near 0.72, and the SD shrinks about ten times.

Monte Carlo repeated batches approximate behavior under the assumed generating process. Increasing N from 100 to 10,000 multiplies sample size by 100, so the standard-error scale shrinks by about sqrt(100) = 10. That convergence and the recorded seed do not prove that p = 0.72 matches production traffic. For parallel replications, give each replica its own recorded stream rather than treating one seed as evidence of independence.

8.An LLM runtime uses temperature T=0.3T = 0.3T=0.3 with base logits [2.2, 1.5, 0.2, -1.5] for [approved, pending, blocked, coupon] and samples via the Gumbel-Max trick i∗=argmax⁡i(zi/T+gi)i^* = \operatorname{argmax}_i(z_i/T + g_i)i∗=argmaxi​(zi​/T+gi​) where gi=−ln⁡(−ln⁡(ui))g_i = -\ln(-\ln(u_i))gi​=−ln(−ln(ui​)). What does this setup accomplish?

Correct answer: Dividing by T=0.3T=0.3T=0.3 concentrates over 90% probability mass on approved, and the Gumbel-Max argmax draws an exact categorical sample in parallel without computing sequential CDF prefix sums.

Temperature T<1.0T < 1.0T<1.0 divides logits by a fraction, stretching gaps and concentrating mass on the mode (approved climbs from 60.4% to 91.1%). The Gumbel-Max trick adds independent standard Gumbel perturbations gi=−ln⁡(−ln⁡(ui))g_i = -\ln(-\ln(u_i))gi​=−ln(−ln(ui​)) directly to the scaled logits; taking the parallel argmax yields an exact categorical sample from softmax⁡(z/T)\operatorname{softmax}(\mathbf{z}/T)softmax(z/T) without requiring GPU thread-divergent sequential CDF prefix sums.

9.A one-server baseline has arrival rate lambda = 19 requests per second and exponential service rate mu = 20 requests per second. Under the M/M/1 formulas, what do utilization and average total time imply?

Correct answer: Utilization is 0.95, total mean time is 1.00 s, and mean queue time is 0.95 s.

Utilization is lambda / mu = 19 / 20 = 0.95. For a stable M/M/1 queue, average total time is W = 1 / (mu - lambda) = 1 second, far above the 1 / mu = 0.05-second mean service time. The gap shows how waiting rises sharply as arrivals approach capacity.

10.A symmetric Metropolis-Hastings proposal targets the earlier Beta(10,4) posterior. Moving from p = 0.5 to p = 0.6 gives target-density ratio 2.642. What follows?

Correct answer: Forward acceptance is 1.000; reverse acceptance is 0.379; retain repeated states after rejection.

Cap the target-density ratio at one. Rejection leaves the chain at its current state for another iteration; discarding those repeats changes the sampled process. Retained states are dependent, so chain length isn't an independent sample count.

10 questions remaining.

Next Step
Continue to Hypothesis Tests, Intervals, and pass@k

You can now simulate Bernoulli pass/fail outcomes and see batch rates wobble even when the generating probability is fixed. The next lesson turns that wobble into a decision rule for whether one model is actually better than another, including <span data-glossary="pass-at-k">pass@k</span> under a fixed sampling budget.

PreviousStatistics and Uncertainty
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

random: Generate pseudo-random numbers

Python Software Foundation · 2026

https://docs.python.org/3/library/random.html

Machine Learning: A Probabilistic Perspective.

Murphy, K. P. · 2012

https://probml.github.io/pml-book/book0.html

NIST/SEMATECH e-Handbook: Binomial Distribution

NIST/SEMATECH · 2012

https://www.itl.nist.gov/div898/handbook/eda/section3/eda366i.htm

NIST/SEMATECH e-Handbook: Poisson Distribution

NIST/SEMATECH · 2012

https://www.itl.nist.gov/div898/handbook/eda/section3/eda366j.htm

NIST/SEMATECH e-Handbook: Lognormal Distribution

NIST/SEMATECH · 2012

https://www.itl.nist.gov/div898/handbook/eda/section3/eda3669.htm

Pattern Recognition and Machine Learning.

Bishop, C. M. · 2006

https://www.microsoft.com/en-us/research/publication/pattern-recognition-machine-learning/

Parallel random number generation

NumPy Developers · 2026

https://numpy.org/doc/stable/reference/random/parallel.html

Monte Carlo Sampling Methods Using Markov Chains and Their Applications

Hastings, W. K. · 1970 · Biometrika

https://www2.stat.duke.edu/~sschmid/Courses/Stat376/Papers/Basic/Hastings1970.pdf

Posterior Analysis

Stan Development Team · 2026

https://mc-stan.org/docs/reference-manual/analysis.html

Introduction to Queueing Theory: 6.263/16.37, Lectures 5 and 6

Eytan Modiano, Massachusetts Institute of Technology · 2026

https://web.mit.edu/modiano/www/6.263/lec5-6.pdf

scipy.stats.goodness_of_fit

SciPy Developers · 2026

https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.goodness_of_fit.html

Deep Learning.

Goodfellow, I., Bengio, Y., Courville, A. · 2016

https://www.deeplearningbook.org/

The Curious Case of Neural Text Degeneration.

Holtzman, A., et al. · 2020 · ICLR 2020

https://arxiv.org/abs/1904.09751

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.