← Startups.com Interview Insights

Startups.com·Machine Learning Engineer·Technical Phone Screen·Senior

Senior
Jul 2026

Summary

Technical screen for an ML Engineer role at Startups.Com, heavy on PyTorch internals and distributed training. Four questions, all deep end of the pool, no warmup.

Questions Asked (4)

Q1

You have a PyTorch training script for CIFAR-10 that either fails to converge, produces NaN/Inf loss, or trains way slower than it should. Walk through how you'd systematically debug it.

Root Cause AnalysisTechnical Trade-offs
Author's notes

This one I actually felt decent about.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by categorizing the symptom (NaN/Inf, non-convergence, or slow training) and then systematically isolate the cause by checking data, model, loss, and optimization in order. Emphasize a hypothesis-driven approach: make one change at a time, use small-scale experiments, and leverage debugging tools like gradient checking and profiling.

Pro tip: Before diving deep, always run a quick sanity check: try to overfit a tiny subset (e.g., 10 samples) with a simple model. If it fails, the bug is likely in the data pipeline or loss; if it succeeds, the issue is in scaling or optimization.

1. Reproduce and Isolate

Reproduce the issue with a minimal setup (e.g., small batch, few epochs) and isolate whether it's data, model, loss, or optimizer related. Use deterministic settings and log key metrics.

2. Check Data and Preprocessing

Verify data loading, normalization, and augmentation. Ensure inputs are correctly scaled (e.g., CIFAR-10 mean/std) and labels are not corrupted. Visualize a few samples and check for NaNs.

3. Inspect Model and Initialization

Check for proper weight initialization (e.g., Kaiming/He), activation functions, and architecture. Ensure no exploding/vanishing gradients by monitoring gradient norms.

4. Validate Loss and Optimization

Confirm loss function is appropriate (e.g., CrossEntropyLoss for classification) and learning rate is reasonable. Try a learning rate finder or reduce LR on plateau. Check for NaN in loss and gradients.

5. Profile and Optimize Performance

Use PyTorch profiler to identify bottlenecks (data loading, GPU utilization, etc.). Ensure data loading uses multiple workers, pin_memory, and that model is on GPU. Consider mixed precision if applicable.

Key Points to Mention

  • Data normalization and augmentation correctness (e.g., CIFAR-10 mean/std).
  • Weight initialization techniques (Xavier, Kaiming) and their impact on convergence.
  • Learning rate scheduling and warm-up strategies.
  • Gradient clipping to prevent exploding gradients and NaN loss.
  • Using torch.autograd.set_detect_anomaly(True) to pinpoint NaN sources.
  • Profiling tools (torch.profiler, nvprof) to diagnose slow training.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q2

How would you scale that same training job to multiple GPUs using Fully Sharded Data Parallel (FSDP)? Cover initialization, model wrapping, optimizer state, gradient accumulation, and checkpointing.

System DesignTechnical Trade-offs
Author's notes

Knew the broad strokes but got tripped up on the gradient accumulation piece specifically.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by outlining the FSDP wrapping strategy, including auto-wrap policies and mixed precision, then detail how optimizer states and gradients are sharded. Finally, explain how gradient accumulation interacts with FSDP and how to implement efficient checkpointing with sharded state dicts.

Pro tip: Mention that FSDP requires careful handling of gradient accumulation to avoid synchronization overhead, and that using `sync_module_states` and `FullStateDictConfig` can simplify checkpointing and loading.

1. Initialization and Setup

Initialize the distributed process group and set up the FSDP environment, including device placement and mixed precision policies.

2. Model Wrapping with FSDP

Wrap the model using FSDP with appropriate auto-wrap policies (e.g., transformer-based) and configure sharding strategy (e.g., FULL_SHARD).

3. Optimizer and Gradient Handling

Use a sharded optimizer (e.g., FSDP's built-in or torch.distributed.optim) and manage gradient accumulation with no_sync context to reduce communication.

4. Checkpointing and Loading

Implement checkpointing using FSDP's state_dict APIs, ensuring sharded states are saved and loaded correctly, possibly with FullStateDictConfig for consolidation.

Key Points to Mention

  • FSDP auto-wrap policy and sharding strategy (e.g., FULL_SHARD, SHARD_GRAD_OP)
  • Mixed precision training with FSDP (e.g., torch.cuda.amp or FSDP's mixed_precision)
  • Optimizer state sharding and using torch.distributed.optim or FSDP's optim_state_dict
  • Gradient accumulation with no_sync context to avoid unnecessary all-reduce
  • Checkpointing with FSDP: saving sharded state dicts and using FullStateDictConfig for consolidation
  • Handling of non-sharded parameters (e.g., via ignored_modules) and synchronization of module states

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q3

How would you implement or approximate sparse gradient all-reduce across workers? What communication patterns would you use, what are the tradeoffs versus dense all-reduce, and when does it actually make sense to do this?

System DesignTechnical Trade-offs
Author's notes

Blanked for a second.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by clarifying the context: sparse gradients arise in models with large embedding tables or Mixture-of-Experts, where only a small fraction of parameters receive updates per step. Then describe a concrete implementation using sparse all-reduce (e.g., all-gather of indices and values, or reduce-scatter with sparse tensors) and compare it to dense all-reduce in terms of communication volume, latency, and complexity. Finally, discuss when sparsity actually pays off—typically when sparsity is high (>90%) and the system is communication-bound—and mention practical challenges like load imbalance and straggler effects.

Pro tip: Emphasize that sparse all-reduce is not a drop-in replacement: it often requires custom communication primitives and careful handling of duplicate indices across workers. Also note that in practice, many frameworks (e.g., PyTorch DDP) still densify gradients for simplicity, so you must justify the engineering effort with a clear cost-benefit analysis.

1. Clarify the scenario and define sparsity

Identify where sparse gradients occur (e.g., embedding layers, MoE) and quantify sparsity level (e.g., 99% zeros). Explain that sparsity is per-worker and may vary, leading to load imbalance.

2. Describe sparse all-reduce implementation

Outline a communication pattern: each worker sends only non-zero (index, value) pairs. Use all-gather to collect all indices/values, then locally aggregate duplicates. Alternatively, use reduce-scatter with sparse tensors if supported.

3. Compare with dense all-reduce

Analyze tradeoffs: sparse reduces communication volume when sparsity is high, but adds overhead from metadata, irregular communication, and potential load imbalance. Dense is simpler and more efficient when sparsity is low or hardware is optimized for dense ops.

4. Discuss when it makes sense

Sparse all-reduce is beneficial when communication is the bottleneck, sparsity is high (>90%), and the model has large, sparse parameters. It may not be worth it for small models or when compute-bound.

5. Mention practical considerations and alternatives

Address challenges: stragglers, duplicate indices, and framework support. Alternatives include gradient compression (e.g., top-k), which can be more general and easier to implement.

Key Points to Mention

  • Sparse gradients typically arise in embedding layers or Mixture-of-Experts models.
  • Communication patterns: all-gather of sparse indices/values, reduce-scatter with sparse support, or parameter server with sparse updates.
  • Tradeoffs: reduced communication volume vs. increased metadata overhead and irregular communication patterns.
  • Load imbalance: some workers may have many more non-zero gradients than others, causing stragglers.
  • When it makes sense: high sparsity (>90%), communication-bound training, large models with sparse parameters.
  • Alternatives: gradient compression (top-k, quantization), which can be more general and easier to implement.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q4

Under what circumstances would you write a custom CUDA or Triton kernel to speed up training, and how would you verify both the performance gain and correctness?

Technical Trade-offsSystem Design
Author's notes

Pretty comfortable here.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by framing custom kernels as a last-resort optimization after exhausting high-level options like torch.compile, fusion, and library kernels. Then describe the specific circumstances (e.g., memory-bound ops, unsupported patterns, fusion opportunities) and outline a rigorous verification process that includes both correctness checks and performance profiling.

Pro tip: Emphasize that you always profile first to identify the bottleneck, and that you consider the maintenance cost and portability trade-offs before writing a custom kernel. Mention that you often prototype in Triton for faster iteration and only drop to CUDA when necessary.

1. Identify the bottleneck

Profile the training loop to find where time is spent (e.g., using PyTorch profiler, Nsight). Determine if the bottleneck is a specific operation that is memory-bound, launch-bound, or not well-optimized in existing libraries.

2. Evaluate alternatives

Before writing a custom kernel, check if existing solutions like torch.compile, cuDNN, or fused optimizers can address the issue. Consider the trade-off between potential speedup and development/maintenance cost.

3. Decide on custom kernel

Write a custom kernel when there's a clear opportunity: e.g., fusing multiple element-wise ops to reduce memory traffic, implementing a novel operation not supported by libraries, or optimizing for specific hardware features (e.g., tensor cores, sparsity).

4. Verify correctness

Compare outputs against a reference implementation (e.g., PyTorch eager) using unit tests with random inputs, checking for numerical equivalence within tolerance. Also test edge cases and gradients if applicable.

5. Measure performance

Benchmark the kernel in isolation and within the full training loop, measuring throughput, latency, and memory usage. Use proper warm-up and multiple runs to account for variance, and compare against the baseline.

Key Points to Mention

  • When to use custom kernels: memory-bound operations, fusion opportunities, unsupported ops, hardware-specific optimizations.
  • Alternatives to consider first: torch.compile, cuDNN, cuBLAS, existing fused kernels.
  • Triton vs CUDA: Triton for faster development and portability, CUDA for maximum control and performance.
  • Correctness verification: unit tests, gradient checks, numerical tolerance, edge cases.
  • Performance verification: profiling tools (Nsight, PyTorch profiler), benchmarking methodology, end-to-end impact.
  • Trade-offs: development time, maintenance burden, portability, and potential for over-optimization.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.