Capstone
Goal
Stand up and tune a self-hosted inference service for a small open-weights model against a stated latency SLO and a realistic workload distribution: continuous batching over a paged KV cache with prefix sharing, an admission and scheduling policy chosen deliberately on the throughput/latency frontier, a documented GPU memory budget, a quantization decision backed by a measured quality evaluation, an autoscaling policy that survives cold starts, full serving observability -- and a defensible build-versus-buy recommendation. The whole thing runs end to end against the committed simulator, offline, with no GPU and no downloaded model weights.
Concepts exercised
- prefill/decode phases and the bandwidth-bound decode (co-02-co-04)
- KV cache arithmetic and its role as the scarce resource (co-05-co-07, co-18)
- paged allocation and prefix sharing (co-08-co-10)
- continuous batching, scheduling, admission control, preemption (co-11-co-14)
- the throughput/latency frontier and the four latency metrics (co-15-co-17)
- quantization evaluated rather than assumed (co-19)
- capacity planning and realistic load testing against a length distribution (co-21, co-22)
- autoscaling with cold starts, packaging, staged model rollout (co-23-co-25)
- serving observability (co-26)
- a total-cost-of-ownership decision (co-27, co-28)
Step 1: serve/ -- serve the model and document the memory budget
Serves the model, instruments the four distinct latency metrics from co-16, and documents the GPU memory budget with the cache arithmetic that yields a maximum concurrency. This step's output feeds directly into Step 2's scheduler as the cache budget it schedules against.
learning/capstone/code/serve/server.py
"""Capstone step 1 -- serve/: serve the model, instrument the four latency metrics, and document the
GPU memory budget with the cache arithmetic that yields a maximum concurrency.
Reuses the exact formulas taught in Examples 1, 11, 14, 16, and 59 -- nothing here is new arithmetic,
only their assembly into one importable module the rest of the capstone builds on.
"""
from dataclasses import dataclass
@dataclass
class TinyModel: # => co-01/co-24: a stand-in for a small, self-hosted open-weights model
name: str = "example-org/example-7b"
def handle_completion_request(model: TinyModel, prompt: str, max_output_tokens: int) -> dict[str, int | str]:
# => co-01: the served endpoint -- request cost is UNKNOWABLE until generation actually finishes
output_tokens = min(max_output_tokens, len(prompt) % 47 + 3) # => deterministic, prompt-dependent length
return {"model": model.name, "prompt_tokens": len(prompt.split()), "output_tokens": output_tokens}
def kv_cache_bytes_per_request(num_layers: int, num_heads: int, head_dim: int, seq_len: int, bytes_per_value: int) -> int:
# => co-06: the cache-size formula from Example 11 / Example 59, unchanged
return 2 * num_layers * num_heads * head_dim * seq_len * bytes_per_value
def gpu_memory_budget(
total_gpu_bytes: int,
weights_bytes: int,
activations_bytes: int,
framework_overhead_bytes: int,
bytes_per_request: int,
) -> dict[str, int]:
# => co-18: weights + cache + activations + overhead must all fit -- the remainder buys concurrency
remainder_for_cache = total_gpu_bytes - weights_bytes - activations_bytes - framework_overhead_bytes
max_concurrency = remainder_for_cache // bytes_per_request # => co-07: cache budget SETS the ceiling
return {
"total_gpu_bytes": total_gpu_bytes,
"weights_bytes": weights_bytes,
"activations_bytes": activations_bytes,
"framework_overhead_bytes": framework_overhead_bytes,
"remainder_for_cache_bytes": remainder_for_cache,
"bytes_per_request": bytes_per_request,
"max_concurrency": max_concurrency,
}
@dataclass
class GenerationTrace: # => co-16: one completed request's four distinct latency metrics
ttft_ms: float
total_ms: float
output_tokens: int
def compute_metrics(trace: GenerationTrace) -> dict[str, float]:
# => co-16: TTFT, ITL, per-user tokens/sec, and (separately) aggregate throughput are FOUR distinct numbers
decode_ms = trace.total_ms - trace.ttft_ms
itl_ms = decode_ms / max(trace.output_tokens - 1, 1)
tokens_per_sec = trace.output_tokens / (trace.total_ms / 1000.0)
return {
"ttft_ms": trace.ttft_ms,
"itl_ms": round(itl_ms, 2),
"tokens_per_sec": round(tokens_per_sec, 2),
}Verify: test_memory_budget_max_concurrency_matches_admission_refusal_point builds a real
ContinuousBatchScheduler (Step 2) against this exact budget's max_concurrency and confirms active
admission never exceeds it -- the documented number and the observed scheduling behavior match.
Step 2: scheduler/ -- continuous batching, paged cache, prefix sharing, preemption
Implements continuous batching over a paged cache with prefix sharing, plus an admission and scheduling policy with preemption under pressure -- and a contiguous, static-batching baseline to measure the improvement against.
learning/capstone/code/scheduler/scheduler.py
"""Capstone step 2 -- scheduler/: continuous batching over a paged cache with prefix sharing, plus an
admission and scheduling policy with preemption under pressure.
Reuses the mechanics taught across Examples 29-46 -- static/continuous batching, paged allocation,
prefix sharing, and preemption -- assembled into one scheduler comparable against a contiguous,
static-batching baseline.
"""
import math
from dataclasses import dataclass, field
BLOCK_TOKENS = 16 # => co-09: fixed block size, same principle as OS virtual-memory paging
@dataclass
class SimRequest:
id: str
prompt_prefix_tokens: int # => co-10: the SHARED portion, if any, with other requests
output_tokens: int
tokens_emitted: int = 0
@property
def finished(self) -> bool:
return self.tokens_emitted >= self.output_tokens
def blocks_needed(token_len: int) -> int: # => co-09: round UP to the nearest whole block
return math.ceil(token_len / BLOCK_TOKENS) if token_len > 0 else 0
def paged_cache_bytes(requests: list[SimRequest], bytes_per_token: int) -> int:
# => co-08/co-09/co-10: paged allocation WITH prefix sharing -- the shared prefix's blocks count ONCE
shared_prefixes = {r.prompt_prefix_tokens for r in requests if r.prompt_prefix_tokens > 0}
shared_blocks = sum(blocks_needed(p) for p in shared_prefixes) # => co-10: paid for ONCE, not per-request
unique_blocks = sum(blocks_needed(r.output_tokens) for r in requests) # => co-09: each request's own output
return (shared_blocks + unique_blocks) * BLOCK_TOKENS * bytes_per_token
def contiguous_cache_bytes(requests: list[SimRequest], bytes_per_token: int, max_seq_len: int) -> int:
# => co-08: the naive baseline -- reserves the WORST-CASE length, contiguously, per request, no sharing
return len(requests) * max_seq_len * bytes_per_token
@dataclass
class ContinuousBatchScheduler: # => co-12/co-13/co-14: admission, continuous batching, preemption
max_batch_slots: int
cache_budget_blocks: int
active: list[SimRequest] = field(default_factory=list[SimRequest])
queued: list[SimRequest] = field(default_factory=list[SimRequest])
preemption_count: int = 0
occupancy_trace: list[int] = field(default_factory=list[int])
def _blocks_in_use(self) -> int:
return sum(blocks_needed(r.output_tokens) for r in self.active)
def submit(self, request: SimRequest) -> None:
self.queued.append(request) # => co-13: every arrival is queued first, admitted only when room exists
def _admit_from_queue(self) -> None:
# => co-13/co-14: bound preemption attempts to the active set's own size -- within ONE scheduling
# tick, a well-behaved scheduler preempts each currently-active request at most once, rather
# than cycling indefinitely when demand structurally exceeds the cache budget (see Example 46's
# thrashing failure mode, which this bound is specifically designed to avoid triggering here).
preemption_budget = len(self.active)
while len(self.active) < self.max_batch_slots and self.queued:
candidate = self.queued[0]
if self._blocks_in_use() + blocks_needed(candidate.output_tokens) <= self.cache_budget_blocks:
self.active.append(self.queued.pop(0))
continue
if not self.active or preemption_budget <= 0:
break # => co-13: no room, and no further preemption budget this tick -- candidate stays queued
# => co-14: under pressure, preempt the LARGEST cache holder to try to make room
victim = max(self.active, key=lambda r: r.tokens_emitted)
victim.tokens_emitted = 0
self.active.remove(victim)
self.queued.append(victim)
self.preemption_count += 1
preemption_budget -= 1
def step(self) -> None:
self._admit_from_queue()
for r in self.active:
r.tokens_emitted += 1 # => co-12: every active request takes one decode step
self.occupancy_trace.append(len(self.active))
self.active = [r for r in self.active if not r.finished] # => co-12: retire IMMEDIATELY, not at batch end
def run_to_completion(self) -> int:
steps = 0
while self.active or self.queued:
self.step()
steps += 1
return stepsVerify: test_continuous_batching_over_paged_cache_outperforms_static_over_contiguous attributes
the throughput improvement to the specific idle-slot-elimination mechanism (co-11 vs co-12);
test_paged_cache_with_prefix_sharing_recovers_fragmented_capacity confirms paged_cache_bytes beats
contiguous_cache_bytes on an identical, fragmentation-prone workload;
test_no_request_class_starves_under_the_scheduler drives run_to_completion() and confirms every
submitted request is eventually admitted and finishes -- nothing starves.
Step 3: tune/ -- the throughput/latency frontier and a measured quantization decision
Plots the throughput/latency frontier, selects an operating point against the stated SLO, and makes the quantization decision by evaluating a quantized candidate's measured quality delta -- never a published figure.
learning/capstone/code/tune/tune.py
"""Capstone step 3 -- tune/: plot the throughput/latency frontier, select an operating point against
the stated SLO, and make the quantization decision by evaluating the quantized model on a real,
learner-measured quality delta -- never a published figure.
Reuses the cost model from Examples 33-34 and the quantization decision shape from Examples 51-53.
"""
from dataclasses import dataclass
BASE_STEP_MS = 15.0
MS_PER_EXTRA_SLOT = 1.0
def itl_at_batch(batch_size: int) -> float: # => co-15/co-16: the SAME cost model used throughout this course
return BASE_STEP_MS + MS_PER_EXTRA_SLOT * batch_size
def throughput_at_batch(batch_size: int) -> float:
return batch_size * 1000.0 / itl_at_batch(batch_size)
def frontier(batch_sizes: list[int]) -> list[tuple[int, float, float]]:
# => co-15: (batch_size, throughput, itl_ms) -- the durable trade, traced across configurations
return [(b, round(throughput_at_batch(b), 1), round(itl_at_batch(b), 1)) for b in batch_sizes]
def pick_operating_point(batch_sizes: list[int], itl_slo_ms: float) -> int:
# => co-15: choose the LARGEST batch size that still satisfies the stated SLO -- maximize throughput
# subject to the constraint, never the other way around
candidates = [b for b in batch_sizes if itl_at_batch(b) <= itl_slo_ms]
if not candidates:
raise ValueError(f"no batch size satisfies ITL SLO of {itl_slo_ms}ms")
return max(candidates)
@dataclass
class QuantizationCandidate: # => co-19: same shape as Example 53, but quality_score is MEASURED, not assumed
name: str
memory_gb: float
measured_quality_delta: float # => co-19: the learner's OWN measured degradation, e.g. from an eval suite
def decide_quantization(candidates: list[QuantizationCandidate], max_tolerated_quality_delta: float) -> dict[str, str]:
# => co-19: accept only candidates within the tolerated MEASURED quality delta; prefer smallest memory
accepted = [c for c in candidates if c.measured_quality_delta <= max_tolerated_quality_delta]
if not accepted:
return {"decision": "reject_all", "reason": "no candidate's measured quality delta is within tolerance"}
winner = min(accepted, key=lambda c: c.memory_gb)
return {
"decision": winner.name,
"reason": f"smallest memory ({winner.memory_gb} GB) with measured quality delta {winner.measured_quality_delta} <= tolerance {max_tolerated_quality_delta}",
}Verify: test_operating_point_is_justified_against_the_slo confirms the chosen batch size actually
satisfies the stated ITL SLO, not merely the largest available; test_quantization_decision_cites_a_measured_quality_delta
confirms decide_quantization only ever reasons about a measured_quality_delta field -- there is no
code path that accepts a published benchmark figure instead.
Step 4: capacity/ -- a realistic workload, a load test, and a capacity model
Characterizes the workload's prompt and output length distributions, runs a load test reproducing them, derives a capacity model, and writes an autoscaling policy sized for weight-load cold starts.
learning/capstone/code/capacity/capacity.py
"""Capstone step 4 -- capacity/: characterize the workload's prompt and output length distributions,
run a load test reproducing them, derive a capacity model, and write an autoscaling policy sized for
weight-load cold starts.
Reuses the distribution shape from Example 56, the capacity formula from Example 59, and the
proactive-autoscaling policy from Example 61.
"""
LENGTH_BUCKETS = [ # => co-21: a realistic, deterministic length distribution -- short-dominated, long-tailed
(50, 40),
(200, 35),
(500, 15),
(1500, 8),
(4000, 2),
]
def expand_to_workload(buckets: list[tuple[int, int]]) -> list[int]: # => co-21: materialize as an explicit list
workload: list[int] = []
for length, weight in buckets:
workload.extend([length] * weight)
return workload
def capacity_model(cache_budget_bytes: int, bytes_per_request_at_typical_length: int) -> int:
# => co-07/co-21: the SAME division as Example 59, now driven by a typical (not worst-case) request size
return cache_budget_bytes // bytes_per_request_at_typical_length
def proactive_scale_out_decision(queue_depth: int, threshold: int, cold_start_seconds: float, arrival_rate_per_sec: float) -> bool:
# => co-23: the SAME proactive policy as Example 61 -- project queue growth across the cold-start window
projected = queue_depth + arrival_rate_per_sec * cold_start_seconds
return projected > threshold
def run_load_test(workload_lengths: list[int], max_batch_slots: int) -> int:
# => co-22: a load test that reproduces the REAL length distribution, not a uniform-average shortcut
pending = list(workload_lengths)
active: list[int] = []
steps = 0
while pending or active:
while len(active) < max_batch_slots and pending:
active.append(pending.pop(0))
active = [r - 1 for r in active]
steps += 1
active = [r for r in active if r > 0]
return stepsVerify: test_capacity_model_predicts_the_load_tests_behaviour drives run_load_test with the
SAME 100-request distribution the capacity model was derived from;
test_autoscaling_policy_accounts_for_measured_cold_start confirms the proactive policy scales out
strictly earlier than a naive policy would, at the identical queue depth.
Step 5: operate/ -- packaging, observability, rollout, and the build-versus-buy call
Packages weights, runtime, and configuration as one versioned artifact; wires the serving observability dashboard; executes a staged model rollout with a quality guardrail and a rollback; concludes with the build-versus-buy recommendation, using realistic utilization rather than a full-load assumption.
learning/capstone/code/operate/operate.py
"""Capstone step 5 -- operate/: package weights, runtime, and configuration as one versioned artefact;
wire the serving-observability dashboard; execute a staged model rollout with a quality guardrail and a
rollback; write the build-versus-buy recommendation.
Reuses the deployment-manifest shape from Example 62, the dashboard/diagnosis pattern from Examples
64/74, the staged-rollout-with-guardrail pattern from Examples 63/72/73, and the TCO-sensitivity
calculation from Example 75.
"""
from dataclasses import asdict, dataclass
@dataclass
class DeploymentManifest: # => co-24: weights + runtime + config, versioned as ONE unit
model_id: str
model_revision: str
framework_version_pin: str # => `[Unverified]` -- see this course's Accuracy notes
replica_count: int
max_batch_slots: int
def package_deployment(model_id: str, revision: str, replica_count: int, max_batch_slots: int) -> dict[str, object]:
manifest = DeploymentManifest(
model_id=model_id,
model_revision=revision,
framework_version_pin="[Unverified]-pin-at-deploy-time",
replica_count=replica_count,
max_batch_slots=max_batch_slots,
)
return asdict(manifest)
def build_dashboard(queue_depth: int, batch_occupancy: float, itl_p50_ms: float, preemption_rate: float) -> dict[str, float | int]:
# => co-26: the five signals that explain a serving problem, read together
return {
"queue_depth": queue_depth,
"batch_occupancy": batch_occupancy,
"itl_p50_ms": itl_p50_ms,
"preemption_rate": preemption_rate,
}
def diagnose(dashboard: dict[str, float | int]) -> str: # => co-26: the SAME rule chain as Example 74
if dashboard["preemption_rate"] > 0.3:
return "cache_pressure"
if dashboard["queue_depth"] > 15 and dashboard["batch_occupancy"] < 0.5:
return "undersized_replica_count"
if dashboard["itl_p50_ms"] > 40:
return "oversized_batch"
return "healthy"
def evaluate_rollout_stage(error_rate: float, p99_latency_ms: float, error_guardrail: float, latency_guardrail_ms: float) -> str:
# => co-25: BOTH guardrails must hold for a stage to advance -- either alone is not enough
if error_rate > error_guardrail:
return "halt"
if p99_latency_ms > latency_guardrail_ms:
return "halt"
return "advance"
def should_rollback(old_p99_ms: float, new_p99_ms: float, regression_tolerance: float) -> bool:
# => co-25/co-26: the SAME rollback rule as Example 73, applied to a planted regression
return new_p99_ms > old_p99_ms * (1 + regression_tolerance)
def build_vs_buy_recommendation(
gpu_hourly_rate: float,
tokens_per_second_at_full_load: float,
realistic_utilization: float,
hosted_api_price_per_million_tokens: float,
) -> dict[str, object]:
# => co-27/co-28: the HONEST comparison uses REALISTIC utilization, never full-load utilization
effective_tokens_per_hour = tokens_per_second_at_full_load * 3600 * realistic_utilization
self_hosted_cost = (gpu_hourly_rate / effective_tokens_per_hour) * 1_000_000
recommendation = "self_host" if self_hosted_cost < hosted_api_price_per_million_tokens else "use_hosted_api"
return {
"self_hosted_cost_per_million_tokens": round(self_hosted_cost, 4),
"hosted_api_cost_per_million_tokens": hosted_api_price_per_million_tokens,
"realistic_utilization": realistic_utilization,
"recommendation": recommendation,
}Verify: test_dashboard_signals_explain_distinct_incidents confirms all four planted incidents
(cache pressure, undersized replica count, oversized batch, healthy) are diagnosed distinctly, never
collapsing to the same cause; test_staged_rollout_rolls_back_on_a_planted_regression plants a
latency regression and confirms both the stage halts and the rollback decision fires;
test_build_vs_buy_uses_realistic_utilization_not_full_load confirms the identical GPU at the
identical hourly rate recommends differently at 10% versus 90% utilization.
Run it end to end
learning/capstone/code/test_capstone.py is the pytest suite covering every acceptance criterion
below, one test per criterion, importing all five step modules directly.
$ python3 -m pytest test_capstone.py -v
============================= test session starts ==============================
collecting ... collected 15 items
test_capstone.py::test_serve_returns_a_completion PASSED [ 6%]
test_capstone.py::test_memory_budget_max_concurrency_matches_admission_refusal_point PASSED [ 13%]
test_capstone.py::test_compute_metrics_returns_distinct_latency_signals PASSED [ 20%]
test_capstone.py::test_continuous_batching_over_paged_cache_outperforms_static_over_contiguous PASSED [ 26%]
test_capstone.py::test_paged_cache_with_prefix_sharing_recovers_fragmented_capacity PASSED [ 33%]
test_capstone.py::test_no_request_class_starves_under_the_scheduler PASSED [ 40%]
test_capstone.py::test_operating_point_is_justified_against_the_slo PASSED [ 46%]
test_capstone.py::test_quantization_decision_cites_a_measured_quality_delta PASSED [ 53%]
test_capstone.py::test_capacity_model_predicts_the_load_tests_behaviour PASSED [ 60%]
test_capstone.py::test_autoscaling_policy_accounts_for_measured_cold_start PASSED [ 66%]
test_capstone.py::test_dashboard_signals_explain_distinct_incidents PASSED [ 73%]
test_capstone.py::test_staged_rollout_rolls_back_on_a_planted_regression PASSED [ 80%]
test_capstone.py::test_deployment_manifest_is_versioned_as_one_unit PASSED [ 86%]
test_capstone.py::test_build_vs_buy_uses_realistic_utilization_not_full_load PASSED [ 93%]
test_capstone.py::test_entire_capstone_suite_runs_offline_without_gpu PASSED [100%]
============================== 15 passed in 0.02s ==============================Acceptance criteria
- The service (Step 1's scheduler-bound budget) holds a stated latency SLO under a load test that
reproduces realistic prompt and output length distributions --
test_operating_point_is_justified_against_the_slo,test_capacity_model_predicts_the_load_tests_behaviour. - The documented memory budget's computed concurrency limit matches observed behaviour --
test_memory_budget_max_concurrency_matches_admission_refusal_point. - Continuous batching over a paged cache measurably outperforms static batching over a contiguous
cache on the same workload, with the improvement attributed to the specific mechanism --
test_continuous_batching_over_paged_cache_outperforms_static_over_contiguous,test_paged_cache_with_prefix_sharing_recovers_fragmented_capacity. - The chosen operating point on the throughput/latency frontier is justified against the SLO --
test_operating_point_is_justified_against_the_slo. - The quantization decision cites a quality delta the learner measured, not a published one --
test_quantization_decision_cites_a_measured_quality_delta. - The autoscaling policy accounts for measured cold-start time --
test_autoscaling_policy_accounts_for_measured_cold_start. - A staged model rollout rolls back on a planted quality regression --
test_staged_rollout_rolls_back_on_a_planted_regression. - Every serving observability signal is shown to explain a distinct incident --
test_dashboard_signals_explain_distinct_incidents. - The build-versus-buy recommendation uses realistic utilization and includes idle cost --
test_build_vs_buy_uses_realistic_utilization_not_full_load. - The entire suite runs offline without GPU access, with GPU-only figures drawn from committed
reference measurements (see Examples 54, 60, 69) --
test_entire_capstone_suite_runs_offline_without_gpu.
Done bar
Runnable end to end (offline, simulator plus CPU-servable model) + web-verified: every framework name, GPU price, and quantization figure this capstone touches is illustrative and flagged in this course's Accuracy notes rather than stated as a current market fact.
← Previous: Advanced Examples
Last updated July 25, 2026