Performance Trade-offs & Optimization Strategy Questions
Deciding what to optimize, how far, and at what cost to other qualities. Covers performance vs readability/reliability/cost trade-offs, prioritizing the optimization with the highest payoff, knowing when a system is fast enough, and sequencing optimization work. Emphasizes optimization as a strategic engineering judgment rather than a reflex.
HardTechnical
58 practiced
Implement in Python (asyncio) a BatchScheduler with the API: BatchScheduler(batch_size: int, max_wait_ms: int, worker_fn: Callable[[List[Any]], Awaitable[List[Any]]]). Method submit(payload, priority=0) -> Awaitable[result]. The scheduler should flush a batch when batch_size is met or when the oldest item reaches max_wait_ms. If a higher-priority request arrives, it should allow early preemption of low-priority items (flush sooner) to meet SLAs. Provide a concurrency-safe implementation, explain starvation avoidance, and show how exceptions from worker_fn propagate to callers.
Sample Answer
Approach (brief): maintain a concurrency-safe queue of pending requests (payload, priority, future, enqueue_time). Start a timer when queue becomes non-empty to flush after max_wait_ms based on oldest item. Flush also triggers when batch_size reached or when a higher-priority item arrives (preemption). Use asyncio.Lock + Condition to synchronize. When flushing, call worker_fn with the batch payloads, then set each request's future with corresponding result or exception. Starvation avoidance: preemption flushes low-priority items early when a higher-priority one arrives; additionally, max_wait_ms ensures low-priority items are flushed after waiting long enough.Code:Key points and reasoning:- Concurrency: asyncio.Lock guards queue and timer state. submit and timer both acquire lock.- Flush behavior: flush when batch_size reached, when oldest item exceeds max_wait_ms (timer), or when a higher-priority item arrives and current queue has lower-priority items (preemption).- Exception propagation: if worker_fn raises, we set_exception on each future so callers awaiting submit see that exception. If worker_fn returns a list but mismatched length, raise and propagate.- Starvation avoidance: max_wait_ms guarantees every item is flushed within SLA regardless of priority. Preemption ensures high-priority requests do not get stuck behind many low-priority ones. Optionally, implement aging (boost priority over time) if stricter fairness required.- Complexity: scheduling overhead O(n) per flush for slicing; worker call cost depends on worker_fn. Memory O(n) for queued requests.- Edge cases: ensure worker_fn returns properly sized list; handle cancellations (futures canceled by callers) as needed (could skip canceled futures when calling worker_fn in advanced variants).
python
import asyncio
import time
from typing import Any, Callable, List, Awaitable, Tuple
class BatchScheduler:
def __init__(self, batch_size: int, max_wait_ms: int, worker_fn: Callable[[List[Any]], Awaitable[List[Any]]]):
self.batch_size = batch_size
self.max_wait_s = max_wait_ms / 1000.0
self.worker_fn = worker_fn
self._lock = asyncio.Lock()
self._queue: List[Tuple[int, float, Any, asyncio.Future]] = [] # (priority, enqueue_time, payload, future)
self._timer_task: asyncio.Task | None = None
async def submit(self, payload, priority: int = 0) -> Any:
fut = asyncio.get_running_loop().create_future()
now = time.monotonic()
async with self._lock:
# append; keep insertion order for same priority
self._queue.append((priority, now, payload, fut))
# start timer if first
if len(self._queue) == 1:
self._start_timer()
# preemption: if new request has higher priority than any existing (lower number = lower priority)
min_priority = min(p for p,_,_,_ in self._queue)
if priority > min_priority:
# flush earlier to avoid high-priority waiting behind many low-priority ones
await self._flush_locked()
elif len(self._queue) >= self.batch_size:
await self._flush_locked()
return await fut
def _start_timer(self):
if self._timer_task and not self._timer_task.done():
self._timer_task.cancel()
self._timer_task = asyncio.create_task(self._timer_worker())
async def _timer_worker(self):
try:
async with self._lock:
if not self._queue:
return
oldest_time = min(t for _, t, _, _ in self._queue)
wait = max(0.0, self.max_wait_s - (time.monotonic() - oldest_time))
if wait > 0:
await asyncio.sleep(wait)
async with self._lock:
if self._queue:
await self._flush_locked()
except asyncio.CancelledError:
return
async def _flush_locked(self):
# must be called while holding self._lock
if not self._queue:
return
batch = self._queue[:self.batch_size]
self._queue = self._queue[self.batch_size:]
# cancel & restart timer for remaining
if self._timer_task and not self._timer_task.done():
self._timer_task.cancel()
self._timer_task = None
if self._queue:
self._start_timer()
priorities, times, payloads, futures = zip(*batch)
# release lock while calling worker_fn
self._lock.release()
try:
results = await self.worker_fn(list(payloads))
if not isinstance(results, list) or len(results) != len(futures):
raise RuntimeError("worker_fn must return a list with same length as input")
for fut, res in zip(futures, results):
if not fut.done():
fut.set_result(res)
except Exception as exc:
for fut in futures:
if not fut.done():
fut.set_exception(exc)
finally:
# reacquire lock to preserve caller expectations
await self._lock.acquire()EasyTechnical
55 practiced
What is model warmup and why does it matter for latency-critical serving? Describe practical warmup strategies for CPU and GPU models (for example: synthetic requests to load kernels, preloading weights into memory, triggering JIT compilation) and discuss the operational costs and trade-offs of keeping warm instances vs on-demand cold starts.
Sample Answer
Model warmup is the set of actions taken before a model begins serving real user requests so its first responses meet latency targets. Warmup matters because many frameworks and hardware paths incur one-time costs—loading weights into RAM, JIT/AOT compilation, CUDA kernel just-in-time linking, memory allocation, and caching—which can make the first request (a cold start) tens to hundreds of milliseconds or even seconds slower than steady-state.Practical warmup strategies- CPU models: - Preload model binaries and tokenizer/vocab into memory and mmap files. - Run a few synthetic inference requests (representative input shapes) to populate caches and trigger lazy init. - Trigger JIT/AOT compilation (e.g., TorchScript tracing, XLA AOT) ahead of time.- GPU models: - Preload weights to GPU memory (cudaMemcpy) and reserve memory pools to avoid fragmentation. - Run synthetic batches to load/cuBLAS/cuDNN kernels and populate GPU kernels and kernel caches. - Run several forward passes that exercise different input sizes to prime autotuners.- Framework-specific: call framework init APIs (torch.cuda.init(), tf.function tracing) and warm any multi-thread/worker pools.Operational costs and trade-offs- Keeping warm instances: - Pros: predictable, low p99 latency; avoids jitter from cold starts. - Cons: constant resource cost (CPU/GPU idle memory and possibly expensive GPU-hours), lower utilization.- On-demand cold starts: - Pros: lower steady-state cost, better utilization. - Cons: higher tail latency, poor UX for infrequent traffic, complexity in retry/backoff and request queuing.- Hybrid approaches: - Keep a small pool of warm instances (autoscale by traffic), use bursty on-demand workers, or schedule warmups during expected traffic spikes. - Use fast checkpoint formats, smaller warm replicas for routing/validation, or lazy-load heavy components.Choose based on SLA (p99 latency), traffic predictability, and cost budget; measure warmup time and cost empirically and automate warm pools accordingly.
HardTechnical
69 practiced
Design an experiment to quantify the cost-versus-accuracy trade-offs introduced by various quantization and pruning configurations for a production model. Specify experiment design (datasets, sample sizes), metrics (inference latency, memory, throughput, accuracy metrics), statistical tests to compare variants, and how you would present a Pareto frontier to stakeholders to guide decisions.
Sample Answer
Requirements & objective:- Quantify accuracy vs cost (latency, memory, throughput) across quantization (FP16, INT8, dynamic/static, per-channel) and pruning (structured/unstructured, sparsity levels 20–90%) for a production model. Goal: identify Pareto-optimal configurations that meet SLOs.Experiment design:- Datasets: use held-out production-like validation set (N_val = 50k examples) + stratified subsets for edge cases (rare classes, long inputs) totalling 10k. Use a calibration set (1k–5k) for post-training quantization where needed.- Sample sizes: power analysis targeting detecting a 0.5% absolute accuracy drop with 80% power and α=0.05; typically ~5k–10k labeled examples per variant for classification; for latency throughput, run 1k–10k runs per config to estimate mean/CI.- Variants: baseline (FP32 dense), FP16, INT8 (per-tensor/per-channel), mixed precision, pruning levels [20,40,60,80] for structured and unstructured, and combos (prune→quantize).Metrics:- Accuracy: primary business metric (e.g., top-1, F1, calibration/Brier), plus class-wise recall for critical classes.- Efficiency: median and 95th-percentile inference latency (cold/warm), throughput (inferences/sec), peak and working memory (RAM/VRAM), model size (MB), energy or cost-per-inference (cloud $/1k inferences).- Robustness: accuracy on edge-case subset, distribution-shift test.Measurement protocol:- Isolate hardware: run on representative hardware (GPU/CPU/edge device); fix batch sizes; repeat runs at steady-state; warm-up runs discarded.- Determinism: fix seeds, use same runtime libraries.- Collect at least 30 independent runs per metric to estimate variance.Statistical analysis:- For accuracy differences: paired t-test or Wilcoxon signed-rank (non-normal) between baseline and variant; report mean difference, 95% CI, and effect size (Cohen’s d).- For latency/throughput: bootstrap CIs; compare medians with Mann–Whitney U if non-normal.- Multiple comparisons: control FDR with Benjamini–Hochberg or use Bonferroni for conservative tests.- Power & practical significance: check if differences exceed business thresholds (e.g., >1% accuracy drop or >20ms latency increase).Pareto frontier & stakeholder presentation:- Plot accuracy (Y-axis, descending) vs cost metric (X-axis): create separate frontiers for different cost axes (latency, memory, $/inference). Each point labeled by config (quant/prune level), with color for reliability (edge-case accuracy).- Show convex hull (Pareto frontier) and annotate SLO boundaries (e.g., max latency, min accuracy). Include table summarizing top Pareto-optimal candidates with trade-offs and recommended options: conservative (minimal accuracy loss), aggressive (max cost savings), balanced.- Provide decision rubric: if accuracy drop ≤ X and cost saving ≥ Y → approve; else require A/B test in production.- Deliverables: reproducible benchmark script, raw metrics, statistical report, interactive dashboard allowing stakeholders to filter by hardware/SLO.This design yields statistically rigorous, reproducible comparisons and a clear Pareto-based decision framework aligned with production constraints.
MediumSystem Design
74 practiced
Design an observability plan for model-serving that includes real-time latency dashboards, error budgets, data-drift detectors, resource-level metrics (GPU/CPU/memory), and automated mitigation actions (scale-out, degrade to baseline model). Which signals should trigger automated mitigation versus human alerts, and why?
Sample Answer
Requirements:- Real-time observability for model-serving: latency (P95/P99), error rate, per-model accuracy/quality proxies, data-drift, GPU/CPU/memory, throughput, and ability to trigger automated mitigations (scale-out, fallback to baseline).- Low-latency dashboards, alerting, and audit logs for mitigation actions.High-level architecture:- Inference layer (model pods) → Metrics exporter (Prometheus) & tracing (OpenTelemetry) → Streaming probe for request/response traces (Kafka) → Observability pipeline: real-time aggregator (Prometheus + Cortex/Thanos), feature-store-derived data-drift service, ML-specific monitor (Seldon/WhyLabs) → Alerting engine (Alertmanager) + Automation runbook executor (Argo Workflows / Kubernetes operator) → Dashboards (Grafana) & ticketing.Signals and detectors:- Latency: P50/P95/P99, tail latency spikes, request queue length.- Errors: HTTP 5xx, model inference exceptions, timeouts.- Quality proxies: feedback labels, surrogate loss, prediction confidence distributions, business metrics.- Data drift: population/statistical drift (KS, PSI), feature distribution shift, semantic drift (embedding distance) with rolling windows.- Resource metrics: GPU util, GPU memory, CPU, memory, GPU temperature, OOM events.Automated mitigation vs human alerts (with rationale):- Automated mitigations (fast, reversible, low-risk): - Sustained P95/P99 > SLA threshold for T_short (e.g., 30s) AND high CPU/GPU saturation or queue growth → scale-out replicas or increase pod resources. Reason: immediate capacity issue, reversible. - Sudden spike in inference errors correlated with new deployment (canary failure) → automated rollback to previous revision. Reason: deployment rollback is deterministic and time-critical. - Sudden drop in model confidence + concurrent resource saturation → degrade to baseline/lightweight model to preserve availability. Reason: preserves service with known behavior.- Human alerts (require investigation, high-risk or ambiguous): - Gradual data drift (statistical change over days) without immediate quality degradation → notify ML engineers for retrain/analysis. Reason: false positives common; retraining costly. - Drop in business KPIs or offline accuracy metrics derived from labels (requires labeling) → human investigation. Reason: requires root-cause and possibly data fixes. - Unexplained anomalies in embeddings/semantic drift or adversarial signal → human alert. Reason: may indicate upstream data poisoning or model failure needing careful handling. - Repeated automated mitigation failures or high-frequency rollbacks → human escalation.Alerting policy and thresholds:- Use multi-signal correlation to avoid flapping: require 2/3 correlated signals (latency + resource OR latency + queue).- Tier alerts: P1 (automated mitigation + pager), P2 (human within SLA), P3 (email/packet).- Maintain error budget: track SLO compliance and block risky auto-actions if budget exhausted; require human approval.Mitigation auditing and safety:- Every auto-action must be logged, annotated with signals that triggered it, and have an automatic rollback window (canary + monitor for 2x metric improvement).- Rate-limit autoscale/fallback frequency and implement circuit-breakers.Dashboards:- Real-time latency/error heatmap per model & endpoint, resource utilization, active mitigations, drift score timeline, SLO/error budget panel, and recent rollback/actions feed.This plan balances fast automated responses for clear operational failures and human oversight for ambiguous, high-impact model-quality issues.
HardTechnical
73 practiced
You must replicate large model checkpoints across data centers for warm standby. Propose a checkpoint transfer and compression strategy that balances network cost and restore latency. Cover options: chunked streaming, delta-compression between checkpoints, quantized or sparse checkpoints, resumable transfers, and integrity verification. Explain trade-offs in restore time and storage overhead.
Sample Answer
Requirements & goals:- Minimize cross-datacenter bandwidth cost while keeping RTO (restore time) low for warm standby.- Ensure transfers are resumable and integrity-checked; support partial restores to start inference quickly.Strategy overview:1. Baseline pipeline- Periodic full checkpoints (e.g., weekly) + frequent incremental checkpoints (deltas) for weights/optimizer state.- Store canonical full checkpoint in source DC and object-store-friendly manifests for chunks/deltas.2. Chunked streaming- Split checkpoints into fixed-size content-addressed chunks (e.g., 8–64MB). Stream chunks over HTTP/GRPC with parallel workers.- Benefits: parallel transfer, pipelined restore (begin loading model before full transfer), dedup across checkpoints.- Trade-off: more metadata, slightly higher request overhead vs monolithic blob.3. Delta-compression between checkpoints- Compute binary diffs between consecutive checkpoints at chunk granularity using content hashing: only changed chunks are sent.- Use block-level dedup and rsync-like rolling checksum for small shifts.- Benefits: drastically reduces bandwidth when fine-tuning or frequent small updates.- Trade-off: extra CPU at source to compute diffs; restore requires applying deltas (adds small latency).4. Quantized / sparse checkpoints- Optionally store quantized (FP16/INT8) or sparsified versions for transfer and “quick warm” restores. Maintain full-precision in cold storage.- Use quantized checkpoint to start inference; asynchronously fetch full-precision chunks to upgrade model.- Trade-off: quantized models may incur accuracy loss; more complex dual-path restore logic.5. Resumable transfers- Use chunk manifests, per-chunk checksums, and checkpoint-level manifest with versioning. Support range requests and multipart uploads.- Maintain transfer state (which chunks present) so interrupted transfers resume at chunk-level.6. Integrity verification- Per-chunk cryptographic hashes (SHA-256) and an overall Merkle tree in manifest to validate piecewise and full integrity after apply.- Verify after delta-apply and after final assembly.Restore time vs storage/bandwidth trade-offs:- Full-checkpoint transfers: fastest restore (no apply step) but highest bandwidth and storage duplication.- Chunked + delta: lowest bandwidth for incremental updates; restore requires fetching base + deltas and applying—adds CPU time proportional to changed chunks.- Quantized-first: fastest usable restore (low-latency warm) but needs background upgrades; saves bandwidth if acceptable accuracy hit.- Resumable + content-addressed chunks: small storage overhead for manifests and chunk index, but large savings via deduplication and partial replication.Operational notes:- Tune chunk size: smaller chunks -> better dedup and resume granularity, higher request overhead; larger chunks -> lower overhead but less efficient diffs.- Monitor change density per checkpoint to decide when to ship full vs delta.- Automate integrity and canary restores regularly to ensure backups are valid.- Consider encryption in transit and at rest; sign manifests for tamper-proofing.This mixed approach gives low steady-state network cost (delta + dedupe), predictable restore latencies (quantized quick-start + background full-precision fill), and robust resumable, integrity-verified transfers.
Unlock Full Question Bank
Get access to hundreds of Performance Trade-offs & Optimization Strategy interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.