Model Optimization, Debugging, and Performance Tuning Questions
Techniques for optimizing machine learning models in production, including hyperparameter tuning, architecture optimization (e.g., pruning, quantization, distillation), and hardware acceleration. Covers profiling and optimizing inference latency, throughput, memory usage, and energy consumption; debugging training instabilities and inference issues; diagnosing data-related problems; ensuring reproducibility and reliability in ML pipelines; and implementing serving optimizations (batching, caching, parallelization) within ML platforms and MLOps workflows.
HardTechnical
55 practiced
You switched training to mixed-precision with distributed multi-GPU training and start seeing inconsistent training results and occasional divergence between runs. Identify the likely causes (non-deterministic ops, unscalable loss scaling, all-reduce numeric differences) and describe a concrete plan to make distributed mixed-precision training more reproducible while balancing performance.
Sample Answer
Likely causes (short):- Non-deterministic ops: some CUDA/cuDNN kernels, atomicAdds, certain reductions, non-deterministic RNG across ranks (dropout, dataloader shuffling).- Unscalable/unstable loss scaling: dynamic loss-scaling decisions diverge between runs/ranks causing different gradient magnitudes or INF/NaN handling.- All-reduce numeric differences: different reduction order across GPUs, mixed-precision casts before/after all-reduce, or using lower-precision for the collective causing fp16 rounding variance.Concrete reproducibility plan (ordered, pragmatic — preserves performance where possible):1) Repro seed & deterministic settings- Fix all RNGs: Python, NumPy, torch, framework-specific RNG per rank (use rank-dependent seeds derived from a base seed).- Disable nondeterministic autotuning: torch.backends.cudnn.benchmark = False- Enable deterministic kernels where available: torch.use_deterministic_algorithms(True) and torch.backends.cudnn.deterministic = True- Set CUBLAS workspace for deterministic GEMMs: export CUBLAS_WORKSPACE_CONFIG=:4096:82) Make data pipeline deterministic- Deterministic dataloader ordering: set worker_init_fn to seed each worker with base_seed + rank and disable shuffle randomness across runs unless seeded.- Ensure any data augmentation uses seeded RNGs.3) Control non-deterministic ops- Identify offending ops (run with torch.use_deterministic_algorithms to surface errors). Replace non-deterministic ops with deterministic implementations (e.g., deterministic pooling/upsampling, avoid certain fused ops).- For dropout: ensure same per-rank determinism or disable during debugging.4) Stabilize loss scaling- Prefer static loss-scaling vs dynamic when debugging reproducibility. If dynamic is required, ensure identical dynamic-scaling hyperparameters and that overflow handling is synchronized: detect and broadcast scale decisions or use per-step global scale policy.- Keep master FP32 weights and do gradients accumulation in FP32 where possible (AMP + GradScaler with consistent settings).5) Make all-reduce numerically consistent- Perform all-reduce in FP32: cast gradients to float32 before all-reduce and cast back if needed. This reduces rounding variance and is cheap relative to instability.- Use deterministic collective implementations: set NCCL to deterministic where available, and ensure same collective algorithms across runs (NCCL env vars can force algorithms).- Avoid per-GPU reorderings: deterministic gradient ordering (consistent parameter iteration order) and avoid async overlapped reductions until stable.6) Synchronize optimizer/FP32 state- Keep optimizer state updates deterministic: ensure consistent ordering, same floating-point dtype for moments (use FP32 for Adam states).- If using gradient accumulation, synchronize steps across ranks carefully.7) Debugging workflow (fast iterations)- Reproduce divergence on a small scale: single-node multi-GPU with minimal config, disable perf optimizations, run multiple seeds to isolate cause.- Toggle one change at a time: deterministic kernels, FP32 all-reduce, static loss scale to see which fixes divergence.- Use per-step value checks: log norms, max/min gradients, loss, GradScaler state across ranks.8) Balance performance vs determinism- For production training where speed matters: restore selective performance flags once behavior is stable. Keep FP32 all-reduce (small perf hit but large stability gain). Re-enable fused ops only after tests. Use selective deterministic settings (only for problematic ops).- Automate: a reproducibility mode (slower) for debugging and a fast mode (benchmarked) for production with monitored checks (gradient norm drift alerts, occasional deterministic runs).Quick PyTorch snippet (seed + deterministic):Expected trade-offs:- Deterministic kernels + FP32 all-reduce increase runtime/memory slightly but dramatically improve reproducibility.- Static loss scaling may reduce some performance but simplifies debugging.- Gradual re-introduction of performance optimizations after validation balances speed and stability.This plan gives a reproducible baseline, a stepwise debugging path, and clear trade-offs to restore performance once correctness is ensured.
python
import os, random, numpy as np, torch
seed = 12345
rank = int(os.environ.get("RANK", "0"))
base = seed + 1000*rank
random.seed(base); np.random.seed(base); torch.manual_seed(base)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
torch.use_deterministic_algorithms(True)
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"HardTechnical
47 practiced
Production GPUs begin to run out of memory over time and eventually OOM, despite no single large request. Outline how you would investigate memory growth (distinguishing caches from leaks) across Python, CUDA, and lower-level C++ layers. Include tooling, experiments to reproduce, and likely fixes for each root cause.
Sample Answer
Situation: Production GPUs steadily run out of memory over hours/days, no single large batch triggers OOM. Need to determine whether growth is from benign caches/pools or from true leaks at Python/CUDA/C++ layers, reproduce it, and fix.Investigation plan (high level)- Reproduce with a minimized loop that mimics production workload (same batch sizes, frequency, inference/training modes).- Add deterministic logging of GPU memory over time (nvidia-smi --query, torch.cuda.memory_allocated/reserved, custom allocator stats).- Narrow scope: toggle features (caching, batch aggregation, async I/O) to see which change stops growth.Tooling per layer and experiments1) Python / framework (PyTorch/TensorFlow)- Tools: Python tracemalloc, objgraph, gc (garbage collector), torch.cuda.memory_summary(), torch.cuda.memory_allocated(), nvidia-smi.- Experiments: - Run repro with torch.no_grad() where applicable and explicit del/tensor.cpu() after use, then gc.collect(); observe if memory drops. - Turn off framework caches (where possible) or periodically call torch.cuda.empty_cache() to see if reserved memory reduces. - Use tracemalloc/objgraph to detect accumulating Python objects that hold .data references to tensors.- Likely causes & fixes: - Holding references (caches, caches of outputs, global lists): fix by releasing references, weakrefs, or move caches to CPU/ disk. - Forgotten .item() vs tensor slices or storing tensors in logging: convert to scalars, detach(). Use context managers to scope tensors.2) CUDA layer / allocator behavior- Tools: nvprof / Nsight Systems, Nsight Compute, CUDA Memcheck (cuda-memcheck), torch allocator stats, nvidia-smi sample every second.- Experiments: - Compare reserved vs allocated: if reserved grows but allocated stays flat, it's allocator caching — not leak. If allocated grows, device allocations are increasing. - Force allocator flushes (framework cache clear) and measure; if memory reverts, it's caching/fragmentation. - Reproduce with pinned_memory=False, disable async transfers to see host pinned leak.- Likely causes & fixes: - Caching allocator fragmentation: reuse buffers, pre-allocate pools, or limit pool size; upgrade framework which fixes fragmentation bugs. - Pinned memory leaks from DataLoader: ensure worker processes exit cleanly, disable pin_memory or fix worker loop. - CUDA context leaks from repeated creation of contexts (e.g., torch.cuda.init() per request): reuse contexts or keep a persistent process.3) Lower-level C++/native code- Tools: valgrind/ASan for host-side leaks, cuda-memcheck for device API misuse, ltrace/strace for allocation syscalls, code inspection of custom CUDA kernels and C++ extensions.- Experiments: - Run server under ASan/valgrind to find host-side mallocs not freed. - Audit C++ PyBind11 modules: ensure destructors free device memory (cudaFree) and device streams are destroyed. - Replace custom ops with no-op to see if growth stops.- Likely causes & fixes: - Missing cudaFree/cudamem release in C++ extensions: add proper RAII, ensure destructors run on process termination. - Persistent device-side allocations in kernels or persistent CUDA streams/handles: free on error paths, reuse streams, fix lifetime management.Distinguishing cache vs leak (practical checklist)- Reserved grows while allocated steady => allocator cache (benign). Monitor after empty_cache(): reserved should drop.- Allocated grows persistently => leak at device allocation site or Python reference retention.- Host memory growth correlated with GPU growth => pinned memory leak / DataLoader or host-side leak.- Restart process: if immediate drop to baseline then growth restarts, indicates process-local leak/caching. If OS-level device memory remains used across restarts, check driver/context issues.Operational mitigations- Short-term: periodic restart of worker processes, scheduled torch.cuda.empty_cache(), move caches to CPU, limit per-worker lifetime.- Medium-term: patch code to free tensors, use weakrefs, fix C++ destructors, close CUDA streams.- Long-term: unit tests that stress memory, integration memory regression tests, telemetry (per-request GPU allocation deltas), upgrade runtime/framework to include allocator fixes.SummaryStart by reproducing a minimal loop and instrumenting both framework and system metrics. Use framework APIs (torch.cuda.*), tracers (tracemalloc/objgraph), and GPU profilers (Nsight, cuda-memcheck) to pinpoint whether growth is allocator caching, pinned-memory, Python reference leaks, or native C++ leaks. Apply targeted fixes: release references and detach tensors at Python level, limit or patch allocator caching and reuse buffers at CUDA level, and ensure correct resource management (cudaFree/destructors) in native C++ extensions.
HardTechnical
48 practiced
Describe an algorithm and, if helpful, pseudocode for introducing dynamic early-exits into a neural network so that individual inputs can exit at intermediate layers based on a confidence threshold. Explain training procedure for exit branches and runtime threshold selection to meet latency vs accuracy constraints.
Sample Answer
To add dynamic early-exits, attach lightweight classifier branches at several intermediate layers. At runtime each branch computes a confidence score; if it exceeds a threshold the sample exits returning that branch’s prediction, otherwise it continues. This trades latency for accuracy.Algorithm overview:1. Insert exit heads hi at layers Li; each head = small pooling + linear (or small MLP) producing logits and softmax confidence p_i = max_softmax(logits).2. Training: jointly train backbone and all heads with a weighted multi-task loss: L = L_final + sum_i α_i L_i, where L_i is cross-entropy of exit i. Optionally use curriculum: pretrain backbone, then train heads freezing backbone, then finetune jointly.3. At inference: for input x iterate through layers; at head i compute p_i; if p_i >= τ_i exit; else continue. If no head passes, use final output.Pseudocode (Python-style):Training details:- Loss weighting α_i tuned so early heads learn useful features without hurting final accuracy (start small, increase).- Use deep supervision and auxiliary losses; apply knowledge distillation from final head to early heads (soft targets) to improve calibration.- Regularize exits with temperature scaling and label smoothing to improve confidence calibration.Runtime threshold selection to meet latency vs accuracy:- Collect per-exit validation statistics: for each τ compute cumulative exit rate r(τ) and accuracy a(τ) and expected latency L(τ)=sum_i r_i(τ)*latency_i + (1 - sum r_i)*latency_final.- Formulate constraint: maximize accuracy subject to L(τ) ≤ latency_budget (or minimize latency loss subject to accuracy ≥ target). This is a small constrained optimization; solve greedy: choose per-exit τ_i by scanning thresholds to meet budget, prefer earlier exits with high accuracy-per-latency gain.- Alternatively learn thresholds via reinforcement learning or grid search: optimize expected reward = accuracy - λ * latency on validation set; pick λ via target trade-off.Practical tips:- Calibrate confidences (temperature scaling) before threshold tuning.- Measure hardware-specific latencies including IO and batching.- Monitor per-class behavior; some classes may systematically exit early—consider class-aware thresholds.- Deploy with dynamic batching and fallbacks; log exit statistics for online threshold recalibration.
python
def infer(x, model, thresholds):
for i, layer in enumerate(model.layers):
x = layer(x)
if i in model.exit_layers:
logits = model.exits[i](x)
prob = softmax(logits)
conf = prob.max()
if conf >= thresholds[i]:
return argmax(prob), i
logits = model.final_head(x)
return argmax(softmax(logits)), 'final'MediumTechnical
95 practiced
You are asked to reduce inference latency by 3x for a real-time recommendation API while keeping accuracy drop under 1%. Describe a prioritized, pragmatic plan of options you would evaluate (e.g., architecture changes, quantization, pruning, batching, caching, hardware choices). For each option state expected impact, risk, and order of evaluation.
Sample Answer
Start with measurements: profile p99/p50 latency, CPU/GPU utilization, memory, and breakdown (preproc, model, postproc, I/O). Then evaluate options in this prioritized, pragmatic order:1) Model serving & inference stack optimizations (order: 1)- Actions: switch to optimized runtime (ONNX Runtime/TensorRT/TF-TRT), use fp16 kernels, fuse ops, enable multi-threading/affinity.- Expected impact: 1.5–2.5x latency reduction.- Risk: Low — minimal model change; validate numeric parity (<1% accuracy drift).2) Dynamic batching & request coalescing (order: 1–2, parallel with 1)- Actions: add small dynamic batch window (e.g., 5–20ms), smart batching by endpoint.- Expected impact: 1.2–2x throughput improvement; latency p50 down for batched flows.- Risk: Medium — tail-latency if batching adds wait; must tune for SLA and real-time constraints.3) Quantization (post-training static/int8 or fp16) (order: 2)- Actions: calibrate and deploy int8 or fp16; use per-channel quantization.- Expected impact: 1.5–3x speedup on CPU/inference accelerators; memory reduction.- Risk: Medium — potential accuracy loss; mitigate with calibration and per-layer fallback. Target under 1% drop.4) Distillation / smaller model or architecture tweaks (order: 3)- Actions: train a compact student or prune heads/layers with knowledge distillation.- Expected impact: 2–5x reduction depending on model shrink.- Risk: Higher — requires retraining and validation; longer lead time.5) Structured pruning + sparse inference (order: 3–4)- Actions: remove channels/blocks to enable dense smaller model or use sparse kernels.- Expected impact: 1.5–3x if hardware/runtime supports sparsity.- Risk: Medium–high; hardware support varies, may need retraining.6) Caching and feature engineering (order: 1–2 for quick wins)- Actions: cache top-N recommendations, user-session results, or precompute heavy features; approximate nearest neighbor (ANN) index for embedding lookups.- Expected impact: large for repeat queries — up to 10x for cache hits; reduces load.- Risk: Low–medium; staleness and cache invalidation complexity.7) Hardware choices & autoscaling (order: 2–4)- Actions: move to inference-optimized instances (GPUs with Tensor Cores, TPUs, AWS Inferentia); provision right-sized nodes and autoscale.- Expected impact: 2–10x depending on hardware and model.- Risk: Cost increase; validate cost-per-qps and integration effort.8) Async / hybrid architectures (order: 4)- Actions: return cached/fast fallback recommendations immediately and refresh heavy personalized recompute asynchronously.- Expected impact: perceived latency reduction; better p99.- Risk: User experience trade-offs; requires careful UX and metrics.Validation plan: A/B test each change, measure end-to-end latency and accuracy on held-out production-like traffic, monitor p50/p95/p99 and business metrics. Prioritize low-risk, high-impact (runtime optimizations, caching, batching, quantization) first; pursue retraining-heavy options (distillation/pruning) and hardware changes in parallel if fast wins are insufficient.
HardSystem Design
48 practiced
Design a global, low-latency model serving architecture capable of 100k QPS with 95th-percentile latency under 50ms. Include component choices for model store, inference serving, autoscaling, multi-region traffic routing, caching, warm-starting models, versioning, and rollback strategy. Explain hardware trade-offs (CPU vs GPU vs accelerators) and networking considerations.
Sample Answer
Requirements & constraints- Global, active‑active serving at 100k QPS total, 95th‑percentile latency <50ms.- Support model versioning, canary rollouts & fast rollback, autoscaling, caching, warm starts.High-level architecture- Edge: Global DNS/GSLB (Route53 latency routing or Cloudflare Spectrum + Anycast).- Regional clusters (K8s per cloud region / on‑prem POPs) with local model store replica, inference fleet, Redis cache, metrics/ALERT pipeline.- Control plane: central Model Registry (MLflow/Feast/Artifact store on S3 + immutable versioned artifacts), CI/CD (GitOps), rollout controller.- Inference: NVIDIA Triton / KServe / BentoML serving in containers (gRPC + HTTP) with envoy/istio sidecars for observability.- Observability: Prometheus + Grafana, distributed tracing (Jaeger), latency SLOs and automated rollback hooks.Component choices & rationale- Model store: S3-backed artifact store + model-registry (MLflow or custom) holding metadata, signatures, runtime config and shas. Use object storage for durability and multi-region replication.- Inference serving: Triton for GPU workloads (supports batching, ensemble), KServe for K8s native autoscaling and multi-runtime support. Expose gRPC endpoint for low overhead.- Autoscaling: - Short‑term: K8s HPA with custom metrics (qps per pod, p95 latency) and KEDA for event-driven spikes. - GPU pods: use cluster-autoscaler with GPU‑aware node pools (node groups per accelerator type) and predictive autoscaler (feed recent traffic pattern + calendar). - Prewarm pool: keep N warm instances per model (see warm‑start).- Multi-region traffic routing: Active‑active with latency based routing + regional health checks. Use consistent hashing or locality‑aware routing for sticky sessions if needed. Fall back to nearest region; failover is automatic via GSLB.- Caching: - Edge: CDN + Varnish for deterministic or TTL-able responses. - Regional: Redis / KeyDB for LRU result cache (per user/request signature), colocated with inference to avoid cross-region calls. - Client SDK: short‑circuit common repeated requests.- Warm-starting models: - Keep a minimum pool of loaded model replicas per region (configurable per model) to avoid cold load. Use image + model prefetch on node startup. - Use pre‑warm cronjobs that run real/lightweight inference to “hot” weights and trigger JIT or kernel warmups.- Versioning & rollbacks: - Immutable model artifacts with semantic versions + git tag. - Traffic splitting in control plane: 0→100% via staged rollout (1%, 5%, 25%, 100%) controlled by metrics (p95 latency, error rate, model quality A/B metrics). - Automated canary evaluation with metric thresholds; automatic rollback on violations; allow immediate emergency rollback to previous SHA.- Deployment & CI/CD: - GitOps pipeline builds artifacts, pushes to registry, triggers rollout controller. - Blue/green or canary via service mesh routing rules.Hardware tradeoffs- CPU: lower cost for small models (trees, linear models). Pros: cheap, predictable single‑request latency, easier autoscaling. Cons: poor throughput for large NN.- GPU: best throughput for large neural nets (transformers, CNNs). Pros: high throughput with batching, lower per‑inference latency for heavy models. Cons: cold start time, higher cost, less granular autoscale (node-level).- Accelerators (Inferentia / TPU / Habana): often best cost/throughput ratio for real-time inference at scale; may require specific runtimes and conversion (ONNX/TFRT), and limited portability.Guidance: classify models: - Small / low compute: CPU autoscaled pods with low concurrency. - Medium/large NNs: GPU pods (use Triton + dynamic batching). - Ultra-high throughput & cost sensitivity: explore cloud accelerators (Inferentia) and optimized runtimes.Batching tradeoffs: increases throughput but increases tail latency. Use dynamic batching with max_batch and max_wait_ms tuned to keep p95 <50ms (e.g., max_wait_ms 5–15ms).Networking & latency considerations- Place regional clusters close to users to minimize RTT; aim for <20ms regional RTT budget to leave time for processing.- Use gRPC with HTTP/2 keepalive, connection pooling, and persistent connections to avoid TLS/handshake costs.- Reduce serialization overhead (protobuf, flatbuffers) and enable zero-copy where possible.- Co-locate Redis & model servers in same AZ; avoid cross-AZ synchronous calls.- Use service mesh for fine-grained traffic controls but disable heavy telemetry on hot paths or sample traces.Sizing sketch (back‑of‑envelope)- If p95 target <50ms, assume avg inference CPU time T and overhead H. For a heavy model requiring GPU: - Target per-region QPS = 100k / N_regions. With 10 regions → 10k QPS. - Use GPUs serving ~500–2000 inferences/sec depending on model & batching. For 10k QPS you might need 5–20 GPUs/region. Validate with microbenchmarks.- Always benchmark real models; capacity planning must use representative inputs and concurrency.Failure modes & mitigations- Cold starts: warm pools + prefetch + image optimization.- Autoscale lag: predictive scaling + buffer capacity.- Network partitions: local caches + degraded mode with cached responses or reduced model fidelity.- Unsafe rollouts: instrument canary metrics (latency, accuracy drift) and auto-rollbacks.Key operational practices- Continuous load testing (k6/locust) with production traffic patterns.- Real-time SLO monitoring and on-call runbooks for rollback/scale.- Periodic re-benchmarking after model changes and runtime/library updates.This design focuses on locality (multi-region), pre‑warming and caching to hit p95 <50ms, GPU/accelerator use for heavy models, careful autoscaling (predictive + reactive), and safe deployments (canary + auto rollback).
Unlock Full Question Bank
Get access to hundreds of Model Optimization, Debugging, and Performance Tuning interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.