Assertions and Behavior Verification Questions
Choosing what to assert and how to verify behavior meaningfully. Covers assertion strategy, verifying observable behavior over implementation detail, avoiding weak or over-specified assertions, and using assertion libraries effectively. Emphasizes assertions that fail for the right reasons and give a clear diagnosis.
Mixed precision (FP16/FP32) speeds up training but can introduce numeric instability. Describe tests and runtime assertions you would add to detect instability early (NaNs in activations/gradients, sudden loss spikes). Provide a PyTorch-based example of an assertion that catches overflow/underflow during training with AMP.
Sample Answer
Approach (brief): add lightweight runtime checks and periodic tests to detect NaNs/infs and extreme dynamic-range issues early: 1) immediate per-step assertions for loss, gradients, parameters, and activations; 2) moving-stat monitors to detect sudden loss spikes or exploding gradients; 3) synthetic/edge-case unit tests (very large/small inputs, adversarial gradients) run in CI; 4) automatic fallback to FP32 or gradient-clipping/scale-tuning on failure.
PyTorch example (AMP-aware) — assertions and monitoring:
import torch
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
model.train()
opt = torch.optim.Adam(model.parameters(), lr=1e-4)
# moving average state for spike detection
ema_loss, alpha = None, 0.01
for step, (x,y) in enumerate(dataloader):
x, y = x.cuda(), y.cuda()
with autocast(): # mixed precision forward
out = model(x)
loss = loss_fn(out, y)
# immediate loss checks
if not torch.isfinite(loss):
raise RuntimeError(f"Non-finite loss at step {step}: {loss.item()}")
# update EMA and detect spike
ema_loss = loss.item() if ema_loss is None else (1-alpha)*ema_loss + alpha*loss.item()
if loss.item() > 10 * (ema_loss + 1e-12):
raise RuntimeError(f"Loss spike detected at step {step}: loss={loss.item():.3e}, ema={ema_loss:.3e}")
opt.zero_grad()
scaler.scale(loss).backward()
# check gradients for NaN/inf before unscale
for name, p in model.named_parameters():
if p.grad is not None:
g = p.grad
# g might be a scaled fp32 tensor after scaler; check finite
if not torch.all(torch.isfinite(g)):
raise RuntimeError(f"Non-finite grad in {name} at step {step}")
# optional: per-param norm check (exploding)
if torch.norm(g).item() > 1e6:
raise RuntimeError(f"Exploding grad in {name} at step {step}: norm={torch.norm(g).item():.3e}")
scaler.step(opt)
scaler.update()
# optional: check model params for inf/nan after step
for name, p in model.named_parameters():
if not torch.all(torch.isfinite(p.data)):
raise RuntimeError(f"Non-finite parameter {name} at step {step}")
Key points and reasoning:
- Use autocast for forward pass and GradScaler to avoid underflow; grads are checked after backward but before optimizer step (they are in fp32 after scaling/unscaling).
- Check loss immediately; it's the earliest indicator.
- EMA-based spike detection catches sudden regressions without false positives from routine noise.
- Per-parameter gradient finite and norm checks find both NaNs/infs and exploding gradients.
- CI tests: run small synthetic batches with extreme inputs and verify no exceptions; run FP32 baseline and compare statistics (mean/var) to detect heavy drift.
- On failure, fallback strategies: reduce init_grad_scale, enable dynamic loss-scaling, enable gradient clipping, or switch to full FP32 for problematic layers.
Design an automated process that creates a reproducible minimal failing example for flaky or failing training jobs. Describe what artifacts to capture (seed, code commit hash, small dataset slice, model checkpoint, environment), how to reduce size of artifacts, and an automated script that attempts to reproduce and validate the problem.
Sample Answer
High-level approach: capture a compact, deterministic snapshot of everything needed to reproduce the failure, then automatically reduce artifacts (dataset, model, randomness) to a minimal failing example and run reproducibility attempts in isolated environments until validated or deemed non-reproducible.
Artifacts to capture
- Code: git commit hash + patch of local uncommitted changes (git diff).
- Config: full training config/flags (JSON/YAML).
- Randomness: RNG seeds for Python, NumPy, framework (torch.manual_seed, tf.random.set_seed), CUDA deterministic flags.
- Dataset slice: original input pointers + export of the minimal subset used in the failing run (see reduction).
- Model state: latest checkpoint and a small checkpoint if available.
- Logs & metrics: stdout/stderr, framework logs, stack traces, profiler traces.
- Environment: OS, python version, package versions (pip freeze), GPU driver, CUDA/cuDNN versions, Dockerfile or container image digest.
- Hardware trace: GPU memory, power, host load if available.
Artifact reduction strategies
- Binary-search dataset slicing: run on progressively smaller slices (half-size, quarter) to find smallest subset that still fails.
- Input delta debugging: remove single examples or features and re-run to find minimal trigger set.
- Model size reduction: train shorter epochs, smaller batch, or freeze layers to isolate.
- Deterministic replay: ensure all randomness fixed; if flaky, introduce repeated runs to detect nondeterminism.
- Compress artifacts: use dataset serialization (TFRecord/Parquet) and gzip; store diffs not full repos.
Automated reproduction script (outline)
- Creates clean container (Docker) from captured environment or pulls image.
- Checks out commit and applies patch.
- Restores packages (pip install -r requirements.txt).
- Loads minimal dataset slice and checkpoint.
- Sets all RNG seeds and deterministic flags.
- Runs training script for N attempts (e.g., 5) capturing exit code, logs, and stack traces.
- Validates failure by checking error patterns or metric divergence.
Example reproducibility runner (Python wrapper)
#!/usr/bin/env python3
import subprocess, json, os, shutil, sys, time
CONF = "repro_config.json" # contains commit, patch, seeds, dataset_path, docker_image
with open(CONF) as f:
cfg = json.load(f)
def run_in_container(cmd, mounts):
base = ["docker", "run", "--rm"]
for src, dst in mounts.items():
base += ["-v", f"{src}:{dst}"]
base += [cfg["docker_image"]] + cmd
return subprocess.run(base, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# prepare workspace
subprocess.run(["git", "checkout", cfg["commit"]])
if cfg.get("patch"):
subprocess.run(["git", "apply", cfg["patch"]])
# attempts
for attempt in range(cfg.get("attempts",3)):
env = {"PYTHONHASHSEED": str(cfg["seed"]),
"TORCH_DETERMINISTIC": "1"}
mounts = {os.path.abspath(cfg["workspace"]): "/workspace",
os.path.abspath(cfg["dataset"]): "/data"}
cmd = ["python", "/workspace/train.py", "--config", "/workspace/config.yaml", "--data", "/data/min_slice"]
r = run_in_container(cmd, mounts)
open(f"run_{attempt}.log","wb").write(r.stdout+r.stderr)
if r.returncode != 0:
print("Failure reproduced on attempt", attempt)
sys.exit(0)
print("No failure reproduced")
Validation and CI integration
- Trigger this runner from CI when flaky failure detected (sentry/alert hook).
- If reproduced, capture final minimal artifacts and open an automated issue with artifacts attached and steps to reproduce.
- If unreproducible after N attempts, mark flaky and schedule longer isolation runs with richer telemetry (profiling, hardware counters).
Trade-offs and best practices
- Aim for smallest deterministic artifact set to speed debugging; but preserve provenance (commit, seed).
- Use containers to remove host differences; if hardware-dependent, collect hardware telemetry.
- Automate TTL for stored artifacts and use artifact storage (S3) with content-addressed keys.
This process yields deterministic, small, shareable reproductions that speed root-cause analysis and reduce noise from environmental nondeterminism.
Explain property-based testing and show a concise Hypothesis example that tests a text-normalization function normalize_text(s: str) -> str for properties: idempotence (normalizing twice yields same result) and that output contains no control characters. Provide Hypothesis strategies you would use.
Sample Answer
To test normalize_text(s: str) with property-based testing, we assert general properties instead of example-based outputs. Key properties here: idempotence (normalize(normalize(s)) == normalize(s)) and result contains no control characters (ord(c) >= 0x20 or c in allowed whitespace like '\n','\t' depending on spec). Use Hypothesis strategies that produce varied Unicode input (including surrogates, combining marks, control chars) so the normalizer is exercised broadly.
from hypothesis import given, strategies as st
import unicodedata
# Example normalize_text to test (replace with real implementation)
def normalize_text(s: str) -> str:
# placeholder: strip, NFC, remove control chars except newline
s = unicodedata.normalize("NFC", s.strip())
return "".join(ch for ch in s if unicodedata.category(ch)[0] != "C" or ch in ("\n","\t"))
# Strategies: general unicode text including control characters
text_strategy = st.text() # includes broad Unicode by default
# Tests
@given(text_strategy)
def test_idempotence(s):
once = normalize_text(s)
twice = normalize_text(once)
assert once == twice
@given(text_strategy)
def test_no_control_characters_in_output(s):
out = normalize_text(s)
# Unicode category starts with 'C' for control/other; allow specific whitespace if required
assert all(unicodedata.category(ch)[0] != "C" or ch in ("\n","\t") for ch in out)
Strategies notes:
- st.text() covers wide Unicode. Use st.text(alphabet=..., min_size=..., max_size=...) to focus tests.
- To emphasize control chars, use st.characters(min_codepoint=0, max_codepoint=0x1F) or st.from_regex(r'[\x00-\x1F]', fullmatch=False).
- Combine with st.lists/st.just to create structured inputs if needed.
Reasoning: Hypothesis explores edge cases (empty string, long strings, combining marks, controls). Idempotence ensures deterministic normal form; checking categories prevents leaking control characters into outputs.
When validating model predictions against ground truth in unit or integration tests, describe the difference between asserting exact equality, threshold-based assertions (e.g., accuracy above X), and distributional checks. Provide guidelines for when to use each and how to write assertion messages that help debugging.
Sample Answer
Exact equality
- What: Asserting model output equals a specific value or tensor element-wise.
- When to use: Deterministic, non-learning code (data preprocessing, tokenization, deterministic feature transforms), or tiny models with fixed weights in unit tests. Use when outputs must be identical across runs.
- Caution: Floating-point rounding, non-deterministic GPU ops, and training randomness make exact equality brittle.
Threshold-based assertions
- What: Assert a metric (accuracy, F1, loss) meets a numeric bound, e.g., accuracy >= 0.85.
- When to use: Integration tests that validate model quality after training, regression tests to ensure no major performance degradation, or smoke tests for production models.
- Guidance: Choose margins based on expected variance (use historical metric distribution or bootstrap to set safe thresholds). Prefer >= lower-bound rather than exact values.
Distributional checks
- What: Verify the distributional properties of predictions (confidence histogram, label distribution, KL-divergence to baseline, calibration error).
- When to use: Monitoring model drift, testing generative models, or ensuring outputs follow expected statistical patterns when exact labels are noisy or unavailable.
- Guidance: Use statistical tests (KS, Chi-square) or thresholds on divergence; allow room for sampling variability.
Assertion message best practices
- Include: expected condition, observed value, dataset/seed/version, and suggested next steps.
- Examples:
- Exact: "Expected token_ids == reference_ids, mismatch at index 42: expected 17, got 18; seed=1234, model=v1.2"
- Threshold: "Accuracy 0.812 < required 0.85 on validation set (n=5k); eval_dir=/artifacts/run_2025-11-01; check training convergence or data leak"
- Distributional: "KL divergence=0.18 > 0.1 threshold between current and baseline predictions; sample_size=1000; inspect data drift and input distribution"
Additional tips
- Use tolerances for floats (abs/rel), log observed metrics for post-failure analysis, and combine checks (e.g., threshold + distributional) for robust CI.
Describe steps to create a minimal, reproducible failing example when you encounter a bug in a training pipeline that only occurs on specific data and hardware. What artifacts should you collect and what small scripts or fixtures would you include with a bug report to help engineers reproduce the issue?
Sample Answer
Start by isolating and shrinking the failure until you can reproduce it reliably with the fewest moving parts.
Steps
- Reproduce deterministically: enable deterministic behavior (set random seeds, torch.use_deterministic_algorithms(True), disable non-deterministic CuDNN ops) and document exact seeds.
- Minimize data: find the smallest subset of the dataset that triggers the bug (single example, few batches). If privacy-sensitive, create a synthetic minimal example that preserves offending properties (shape, dtype, special values).
- Minimize model & pipeline: strip to the smallest training loop that still fails (forward, loss, backward, optimizer.step). Remove auxiliary data loaders, augmentations, callbacks.
- Capture environment & hardware: OS, Python, CUDA, cuDNN, driver versions, GPU model, container/runtime (Docker/Colab), installed packages (pip freeze or conda env export), and exact git commit.
- Collect runtime artifacts: console logs, stack traces, CUDA logs, tensor values at relevant points, model checkpoint near failure, profiler traces, core dumps if applicable.
- Create reproducible artifact bundle: minimal dataset (or generator), small script to reproduce, environment spec (Dockerfile or conda.yml), run script and a README with exact commands.
Artifacts and small scripts to include
- reproduce.py: minimal training loop using the same model ops, loads the tiny dataset or generator, sets seeds, and reproduces the failure.
# reproduce.py
import torch, random, numpy as np
torch.manual_seed(0); np.random.seed(0); random.seed(0)
# Minimal model and one-batch training step
model = torch.nn.Linear(128, 10).cuda()
opt = torch.optim.SGD(model.parameters(), lr=1e-3)
x = torch.randn(4,128, device='cuda'); y = torch.randint(0,10,(4,), device='cuda')
out = model(x); loss = torch.nn.functional.cross_entropy(out, y)
loss.backward(); opt.step()
print('done', loss.item())
- generate_synthetic.py: creates the smallest failing inputs (shapes, dtypes, edge values).
- run.sh: exact commands to run reproduce.py, plus env setup instructions.
#!/bin/bash
# run.sh
python -m pip install -r requirements.txt
python reproduce.py
- Dockerfile or conda.yml: pin versions (python, torch, CUDA).
- logs/: crash.log, stderr/stdout, profiler trace (.pt or .json), and any GPU traces (nvidia-smi output).
- git info: git rev-parse HEAD and diff of local changes.
What to note in the bug report
- Exact reproduction steps and commands.
- Minimal input that triggers the bug (attached file or generator).
- Expected vs actual behavior and precise error messages/stack traces.
- Environment and hardware details.
- Frequency (always/sometimes) and any workarounds tried.
- Priority/impact: how it affects training or model correctness.
Why this helps
- Small, deterministic examples remove noise, accelerate debugging, let engineers run locally or in CI, and make bisecting/code inspection feasible.
Unlock Full Question Bank
Get access to all 40 Assertions and Behavior Verification interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.