AI EngineeringZero to ProductionHome·About·Contact
Appendix · Advanced AI Engineering · Part 9

Big Tech AI-Engineering Patterns

The other interview loop — for AI/ML engineer, applied scientist, and LLM/GenAI engineer roles at Big Tech. Beyond rote pattern-drilling (that's D7), these rounds test whether you can implement the ML primitives from scratch, reason about production LLM systems, and design RAG/agent architectures. This part maps each A1–A8 topic to the questions actually asked, with runnable code and diagrams.

⏱️ ~3 hours🎯 AI/ML interview loop🧠 implement + designrunnable

Learning objectives

  • Implement the ML-from-scratch primitives interviewers ask for (softmax, attention, k-means, cosine kNN).
  • Answer the LLM-systems questions: tokenization, sampling, context management, caching, rate limits.
  • Design a RAG pipeline and an agent loop on a whiteboard, with the right trade-offs.
  • Talk fluently about the "productionizing an LLM feature" system-design round.
How this maps to A1–A8Every section below is a Big-Tech-style AI question anchored to the track: A4 → implement softmax/attention/k-means; A5 → tokenization & sampling; A3 → concurrency & streaming; A6 → structured output & resilience; A7 → RAG & vector search design; A8 → productionization & evals. It's the "can you actually build it?" companion to the DSA round in D7.

1 · Implement softmax & cross-entropy (A4) very common

Asked as: "code softmax — now make it numerically stable." The stability trick (subtract the max) is the whole point; forgetting it overflows on large logits.

logits → subtract max (stability) → exp → normalize [2.0, 1.0, 0.1] e^(x−max) [.66,.24,.10] sums to 1 · largest logit → largest probability Softmax = exponentiate then normalize. Subtracting the max before exp shifts values into a safe range without changing the result (it cancels in the ratio) — the detail interviewers look for.
🗺️ How to read this diagram

This picture shows what softmax does: it turns a list of raw scores (called logits) into a list of probabilities that add up to 1. Read it left to right — each arrow is one step of the calculation.

  • Left box [2.0, 1.0, 0.1] — the raw scores the model produced. They can be any size, positive or negative; they are not probabilities yet.
  • First arrow (subtract max, then e^(x−max)) — we shift every number down by the largest one, then raise e (≈2.718) to each. Subtracting the max is the stability trick: it keeps the numbers small enough that the computer won't overflow, and it does not change the final answer.
  • Second arrow (normalize) — divide each result by the total so the whole list sums to exactly 1.
  • Right box [.66, .24, .10] — the probabilities. Notice the biggest input (2.0) became the biggest probability (.66): softmax keeps the ranking, it just rescales into a 0–1 range.

In short: softmax = exponentiate, then divide by the total. The order of the numbers never changes — the largest logit always gets the largest probability.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Numerically stable softmax + cross-entropy loss
pythonimport numpy as np

def softmax(logits):
    z = logits - np.max(logits)          # STABILITY: avoid exp overflow
    e = np.exp(z)
    return e / e.sum()

def cross_entropy(logits, label):
    p = softmax(logits)
    return -np.log(p[label] + 1e-12)     # negative log-prob of the true class

print(softmax(np.array([2.0, 1.0, 0.1])).round(2))   # [0.66 0.24 0.1 ]
▶ How this works

This is the diagram above written as real code, plus its partner cross-entropy — the number that measures how wrong a prediction was. Interviewers ask for softmax and expect the stability trick; forgetting it is the classic mistake.

  1. z = logits - np.max(logits) subtracts the largest value from every logit. This is the stability step — it prevents np.exp from blowing up to infinity on big numbers. Because it cancels out in the division, the final probabilities are identical.
  2. e = np.exp(z) raises e to each shifted value, then e / e.sum() divides by the total so the results add to 1 — the two "exponentiate then normalize" steps from the picture.
  3. cross_entropy calls softmax, then returns the negative log of the probability the model gave to the correct answer (p[label]). A confident-and-right model gets a probability near 1, whose log is near 0 → tiny loss. A confident-and-wrong model gets a tiny probability → huge loss.
  4. The tiny 1e-12 added inside the log is a safety guard: log(0) is undefined, so this keeps it from crashing if a probability rounds to exactly zero.

What the output means: The last line prints [0.66 0.24 0.1 ] — the three probabilities, which sum to 1. That matches the green box in the diagram.

Try this: Change the input to [100.0, 1.0, 0.1] and run it — thanks to the - np.max line it still works. Delete that line and the same input overflows to nan (not-a-number). That one line is the whole point of the question.

🔗 Builds onA4 §8 (sampling) & the autograd engine in A4 §6. Follow-ups: temperature scaling, top-k/top-p sampling, log-softmax, why cross-entropy pairs with softmax.

2 · Implement scaled dot-product attention (A4) the signature question advanced

Asked as: "implement attention" — the single most common GenAI-role question. It's three matmuls, a scale, a softmax, and a weighted sum.

Attention(Q,K,V) = softmax(QKᵀ / √d) · V Q Kᵀ scores /√d softmax · V weights say "how much each token attends to every other" Attention is a weighted average of values. QKᵀ scores every query against every key; dividing by √d keeps gradients stable; softmax turns scores into weights; multiplying by V mixes the values. This is the transformer's core.
🗺️ How to read this diagram

This is the heart of every transformer. Attention lets each word look at every other word and decide how much to pay attention to it. The formula on top (softmax(QKᵀ/√d)·V) is exactly what the boxes below spell out, step by step.

  • Q and Kᵀ boxesQ (queries) is "what each word is looking for"; K (keys) is "what each word offers." The little just means K is flipped so the shapes line up for multiplication.
  • scores /√d box — multiplying Q by Kᵀ gives a grid of scores: how well each word matches each other word. Dividing by √d (the square root of the vector size) keeps those scores from getting so large that the next step breaks.
  • softmax box — turns each row of scores into weights that add up to 1 (same softmax as section 1). Now every word has a set of "how much I care about each other word" percentages.
  • · V boxV (values) is the actual content of each word. Multiplying the weights by V produces a weighted average: each word's output is a blend of the words it cared about most.

In short: Attention is just a weighted average of values, where the weights come from how well queries match keys. Q·Kᵀ scores the matches, √d keeps them stable, softmax turns them into percentages, and ·V mixes the content.

Scaled dot-product attention (self-attention), from scratch
pythonimport numpy as np

def softmax_rows(x):
    x = x - x.max(axis=-1, keepdims=True)
    e = np.exp(x)
    return e / e.sum(axis=-1, keepdims=True)

def attention(Q, K, V, mask=None):
    d = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d)         # (seq, seq) affinities
    if mask is not None:
        scores = np.where(mask, scores, -1e9)   # causal mask → future = -inf
    weights = softmax_rows(scores)        # each row sums to 1
    return weights @ V                    # weighted sum of value vectors

seq, d = 3, 4
Q = K = V = np.random.rand(seq, d)
print(attention(Q, K, V).shape)           # (3, 4)
▶ How this works

Here is the attention diagram as runnable code — "the signature question" for GenAI roles. It is only a handful of lines: a matrix multiply, a scale, a softmax, and another matrix multiply.

  1. softmax_rows is the section-1 softmax applied to each row separately (axis=-1 means "along the last dimension"). Every row of scores becomes its own set of weights that sum to 1.
  2. Inside attention, Q @ K.T / np.sqrt(d) is the picture's first two boxes at once: @ is matrix multiply (Q times K-flipped), and dividing by np.sqrt(d) is the √d scaling that keeps scores stable.
  3. The optional mask line replaces "not allowed" positions with a huge negative number so softmax drives their weight to ~0. This is how a decoder stops a word from peeking at future words it hasn't generated yet.
  4. weights = softmax_rows(scores) turns scores into percentages, and weights @ V is the final · V box — the weighted average of the value vectors.

What the output means: It prints (3, 4) — the output has the same shape as the input (3 words, each a 4-number vector). Attention transforms the vectors; it doesn't change how many there are.

Try this: The Q, K, V here are random, so the numbers are meaningless — the point is the shape. Change seq, d = 3, 4 to 5, 8 and the printed shape becomes (5, 8). In a real transformer, Q/K/V come from the words, not np.random.rand.

Common follow-upsAdd a causal mask (shown above) for decoder attention; extend to multi-head (split d into h heads, attend in parallel, concat); explain why √d scaling matters (keeps softmax out of the saturated region). All build on vectorization from A4.

3 · k-means & cosine kNN (A4, A7) common

Asked as: "cluster these embeddings" / "find the k nearest vectors." Tests whether you can turn the vector math from A4/A7 into a loop.

Cosine kNN + one k-means iteration
pythonimport numpy as np

def knn(corpus, query, k):
    corpus = corpus / np.linalg.norm(corpus, axis=1, keepdims=True)
    query = query / np.linalg.norm(query)
    sims = corpus @ query                 # cosine = dot on normalized vectors
    return np.argsort(-sims)[:k]          # top-k indices

def kmeans_step(X, centroids):
    # assign each point to nearest centroid, then recompute centroids
    d = ((X[:, None, :] - centroids[None, :, :]) ** 2).sum(-1)
    labels = d.argmin(axis=1)
    new = np.array([X[labels == c].mean(axis=0) for c in range(len(centroids))])
    return labels, new                   # repeat until centroids stop moving
▶ How this works

Two classic "do something with these embeddings" questions in one block. kNN finds the vectors most similar to a query; k-means groups vectors into clusters. Both are just loops over vector math.

  1. In knn, dividing each vector by np.linalg.norm(...) rescales it to length 1 ("normalizing"). Once vectors are length 1, a plain dot product is the cosine similarity — a score from -1 (opposite) to 1 (identical).
  2. corpus @ query computes that similarity for every stored vector at once. np.argsort(-sims)[:k] sorts from highest to lowest (the minus flips the order) and keeps the top k — the k nearest neighbours.
  3. In kmeans_step, the big bracketed expression measures the distance from every point to every centroid (cluster centre). d.argmin(axis=1) then labels each point with its closest centroid.
  4. The last line recomputes each centroid as the average of the points assigned to it. Repeating this assign-then-recompute loop is the whole k-means algorithm — the centres shuffle around until they stop moving.

Try this: k-means is two steps on repeat: (1) assign each point to the nearest centre, (2) move each centre to the middle of its points. Picture dropping 3 pins on a map, then sliding each pin to the centre of its nearest towns, over and over.

🔗 Builds onCosine similarity + top-k from A4 §4; k-means is the intuition behind the IVF vector index in A7 §3. Follow-ups: k-means++ init, elbow method, why cosine vs Euclidean for embeddings.

4 · Tokenization & sampling questions (A5) common

Asked as: "why is token count ≠ word count?", "implement top-p (nucleus) sampling", "how would you truncate a prompt to fit the context window?"

Top-p (nucleus) sampling
pythonimport numpy as np

def top_p_sample(probs, p=0.9):
    idx = np.argsort(-probs)                 # high→low
    sorted_p = probs[idx]
    cum = np.cumsum(sorted_p)
    cutoff = np.searchsorted(cum, p) + 1    # smallest set with mass ≥ p
    keep = idx[:cutoff]
    renorm = probs[keep] / probs[keep].sum()
    return int(np.random.choice(keep, p=renorm))   # sample from the nucleus
# temperature: divide logits by T before softmax — higher T = flatter = more random
▶ How this works

When a model picks the next word, it doesn't always take the single most likely one — that would be repetitive. Top-p (nucleus) sampling keeps just enough of the most likely words to cover a probability mass of p (say 90%), then picks randomly from that shortlist.

  1. np.argsort(-probs) orders the words from most to least likely (the minus sorts high→low). sorted_p is those probabilities in that order.
  2. np.cumsum(sorted_p) makes a running total: 0.5, then 0.75, then 0.9… np.searchsorted(cum, p) finds where that running total first reaches p, so cutoff is the size of the shortlist (the "nucleus").
  3. keep = idx[:cutoff] is that shortlist of word indices. Dividing their probabilities by their own sum (renorm) rescales them back to add up to 1, since we threw the rest away.
  4. np.random.choice(keep, p=renorm) then picks one word at random, weighted by those probabilities. The trailing comment notes temperature: dividing logits by a number T before softmax flattens the probabilities — higher T = more surprising word choices.

What the output means: The function returns a single integer — the index of the chosen next word. Run it many times and you'll get different words, but always ones from the high-probability shortlist.

Try this: Set p=0.1 and the shortlist shrinks to basically the single top word (nearly greedy, very repetitive). Set p=1.0 and every word is eligible (more creative, more random). That dial is exactly what "top-p" controls in a real API.

🔗 Builds onBPE tokenization and token counting from A5 §3–4. Follow-ups: greedy vs top-k vs top-p, temperature, repetition penalty, why streaming needs incremental detokenization.

5 · System design: "Design a RAG system" (A7) the design round expert

Asked as: "design a question-answering system over 10M company documents." The interviewer wants the pipeline, the scaling choices, and the failure modes — not code.

offline: ingest → chunk → embed → index · online: retrieve → rerank → generate docs chunk (A5) embed (A4) vector DB (A7) query retrieve k rerank LLM + context answer RAG has an offline and an online path. Offline: chunk → embed → index (rebuild when docs change). Online: embed the query → ANN retrieve top-k → rerank → stuff context → generate with citations. Name the trade-offs at each box.
🗺️ How to read this diagram

This is the whiteboard answer to "design a question-answering system over millions of documents." The trick is that RAG (retrieval-augmented generation) has two separate paths — one you run ahead of time, one you run per question. Read the top row first, then the bottom row.

  • Top row (offline, run once when documents change): docs → chunk → embed → vector DB. You break documents into small pieces (chunk), turn each piece into a vector of numbers (embed), and store those vectors in a searchable vector database. This is slow, so you do it in advance.
  • Bottom row (online, run for every question): query → retrieve k → rerank → LLM + context → answer. The user's question is embedded, the DB returns the k closest chunks (retrieve), a rerank step reorders them best-first, and those chunks are handed to the LLM as context so it answers from real documents instead of guessing.
  • The dashed arrow from the vector DB down to retrieve k shows the link between the two paths: the online search reads the index the offline path built.
  • The (A4), (A5), (A7) tags point at earlier lessons where each box is built from scratch — chunking (A5), embedding (A4), the vector index (A7).

In short: RAG = look it up, then answer. Offline you build a searchable index of your documents; online you fetch the few most relevant pieces and let the model write an answer grounded in them. In the interview, name the trade-off at every box.

What to say in the RAG design roundChunking (size vs recall, overlap — A5). Embeddings (model choice, dimension, cost — A4). Index (exact vs HNSW/IVF; when to shard — A7). Retrieval (hybrid dense+BM25, RRF, metadata filters — A7). Generation (context budget, citations, grounding check — Ch 3/6). Failure modes (stale index, hallucination when retrieval misses, cost blow-up) and evals (retrieval recall@k + answer quality — Ch 5). Mention caching and latency budget (A3/A8).

6 · System design: "Design an LLM agent / tool-use system" (A6, Ch 4) the design round expert

Asked as: "design an assistant that can call tools / take actions safely." They want the loop, the safety gate, and how you keep it reliable.

the agent loop: model proposes → validate → gate → execute → feed result back LLM validate (A6) gate tool exec observation fed back → repeat until done (MAX_STEPS cap) An agent is a loop with guardrails. The model emits a tool call → validate it (Pydantic/discriminated union, A6) → a policy gate decides allow/ask/block → execute → feed the result back. Bound it with MAX_STEPS, idempotency keys, and a human gate on risky actions.
🗺️ How to read this diagram

This shows how an AI agent that can take actions (call tools) stays safe. An agent isn't one call — it's a loop that repeats until the task is done. Follow the arrows left to right, then notice the arrow that curves back.

  • LLM box — the model proposes an action, e.g. "call the delete-file tool." It only suggests; it doesn't get to run anything directly.
  • validate box — check the proposed action has the right shape and arguments (the A6 tag points to the validation lesson). A malformed request is rejected here before anything happens.
  • gate box — a safety policy decides allow, ask a human, or block. Risky or irreversible actions get stopped or require approval; safe ones pass through.
  • tool exec box — the approved action actually runs, and its result (the "observation") is fed back to the LLM via the curving arrow. The caption's MAX_STEPS cap is what stops the loop from running forever.

In short: An agent is a loop with guardrails: propose → validate → gate → execute → feed the result back → repeat. The gate and the step cap are what make it safe to let a model take real actions.

What to say in the agent design roundLoop (propose → act → observe, step cap). Structured tool calls validated before execution (A6 discriminated unions). Safety gate (risk classes, allow/ask/block, human-in-the-loop for irreversible ops — Ch 8). Reliability (retries/backoff, circuit breaker, idempotency — A6). Memory (bounded message window — D2 deque). Observability & evals (trace every step, hard-fail safety evals — Ch 5/A8). This is exactly the capstone in Ch 8.

7 · "Make 1000 LLM calls efficiently" (A3) common

Asked as: "you need to embed a million documents / evaluate 5000 prompts — how, without hitting rate limits?" Tests concurrency + backpressure judgment.

Bounded async fan-out (semaphore + gather)
pythonimport asyncio

async def bounded_map(coro_fn, items, limit=10):
    sem = asyncio.Semaphore(limit)         # cap concurrency → respect rate limits
    async def worker(x):
        async with sem:
            return await coro_fn(x)
    return await asyncio.gather(*[worker(x) for x in items])
# pair with token-bucket rate limiting + retry/backoff on 429s (A6)
▶ How this works

The interview question is "make 1000 LLM calls efficiently without hitting rate limits." You want many calls happening at once (fast), but not too many at once (or the API rejects you). This code runs them in parallel with a fixed ceiling.

  1. asyncio.Semaphore(limit) is a permit counter — it hands out at most limit permits (here 10) at a time. It's the "only 10 at once" rule that keeps you under the rate limit.
  2. Each worker does async with sem: — it waits until a permit is free, takes one, runs its call with await coro_fn(x), then releases the permit so a waiting worker can go. await means "pause here without blocking everyone else."
  3. asyncio.gather(*[worker(x) for x in items]) launches a worker for every item and waits for them all to finish. The semaphore quietly ensures only 10 are actually in flight at any moment; the rest queue up.
  4. The final comment is the senior add-on: pair this with a rate limiter and retry/backoff (wait-and-retry on HTTP 429 "too many requests") for a production-grade fan-out.

What the output means: It returns a list of all the results, in the same order as items — but the calls ran concurrently (up to 10 at a time), so 1000 calls finish far faster than doing them one by one.

Try this: Change limit=10 to limit=1 and the calls run strictly one after another (slow but gentle). Raise it to 100 and they flood out at once — which is exactly what trips a rate limit. The semaphore is the knob that balances speed against the API's limits.

🔗 Builds onAsync, semaphores & backpressure from A3 §4; retry/backoff from A6 §6. Say: I/O-bound → async or threads (GIL released — A2); cap concurrency with a semaphore; add a rate limiter + exponential backoff; stream results as they finish.

8 · "Productionize an LLM feature" (A8) the wrap-up

Asked as: "we shipped a chatbot — how do you make it reliable, cheap, and safe over time?" The senior-signal round.

ConcernWhat to sayRef
Quality doesn't regressgolden-set evals + LLM-judge, gate deploys in CICh 5, A8
Cost controlprompt caching, model routing, token budgetsA8 §8
Latencystreaming, async, cache, smaller model for easy stepsA3
Safetyguardrails in code/IAM, hard-fail safety evals, human gateCh 6, Ch 8
Versioning & rollbackpin model + prompt versions, shadow/canary deployA8 §3
Monitoringlatency/cost/quality/drift dashboards + alertsA8 §7
The senior answerDon't just list features — describe the loop: ship behind an eval gate → monitor quality/cost/latency in prod → feed failures back as new golden cases → iterate. Guardrails live in code and IAM, not the prompt. That closed loop is what separates a demo from a system.

AI-interview question → what they're testing expert

QuestionThey're testingSection
"Implement softmax (stably)"numerical care§1
"Implement attention"transformer fundamentals§2
"Cluster / kNN these embeddings"vector math → code§3
"Implement top-p sampling"decoding knowledge§4
"Design a RAG system"retrieval architecture§5
"Design a tool-using agent"loop + safety design§6
"Make 1000 LLM calls efficiently"concurrency & limits§7
"Productionize an LLM feature"MLOps maturity§8

Checkpoint expert

  • Implement stable softmax, scaled dot-product attention, and cosine kNN / k-means from scratch.
  • Explain tokenization and code a sampling strategy (top-p / temperature).
  • Whiteboard a RAG pipeline and an agent loop, naming trade-offs and failure modes.
  • Answer the "productionize / make it reliable + cheap + safe" round with the eval→monitor→iterate loop.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Implement softmax (numerically stable)Beginner

Context: Softmax is the classic ML-interview warm-up, and the point being tested is the numerical-stability trick: subtracting the max before exponentiating avoids exp overflow on large logits.

Your task: Implement a numerically stable softmax(logits) and confirm its output is a valid probability distribution.

Requirements:

  • Subtract the max logit before exponentiating so all exponents are ≤ 0
  • Return the same result mathematically as the naive version, without overflow
  • The returned values are all non-negative and sum to 1.0
  • Pure stdlib — math.exp is enough, no numpy
  • Print the distribution and its sum to verify it totals 1

💡 Hint: Compute m = max(logits), exponentiate x - m for each, then divide by the sum of those exponentials.

Show solution

Stable softmax subtracts the max before exponentiating (pure stdlib):

import math

def softmax(logits):
    m = max(logits)                       # stability: shift so the max is 0
    exps = [math.exp(x - m) for x in logits]
    s = sum(exps)
    return [e / s for e in exps]

out = softmax([2.0, 1.0, 0.1])
print([round(p, 3) for p in out])         # [0.659, 0.242, 0.099]
print(round(sum(out), 6))                 # 1.0

Without the - m shift, large logits overflow exp; subtracting the max is mathematically identical but keeps exponents ≤ 0. The interviewer is checking you know that stability trick and that softmax yields a probability distribution (sums to 1).

Exercise 2 · Cross-entropy lossIntermediate

Context: Cross-entropy is the standard classification and language-model training loss because it punishes confident mistakes hardest. Pairing it with softmax is the natural follow-up to the softmax warm-up.

Your task: Implement cross_entropy(logits, target_index) as the negative log of the softmax probability of the true class, and show it's low when confident-correct.

Requirements:

  • Turn logits into probabilities with a stable softmax first
  • Return -log(p) of the probability assigned to the target class
  • Loss is near 0 when the model is confidently correct
  • Loss is large when the model is confidently wrong
  • Demonstrate both the correct-target and wrong-target cases numerically

💡 Hint: Reuse your stable softmax, index out the target class's probability, and take -math.log of it.

Show solution

Cross-entropy penalizes low probability on the true class (runnable):

import math

def softmax(logits):
    m = max(logits); exps = [math.exp(x-m) for x in logits]; s = sum(exps)
    return [e/s for e in exps]

def cross_entropy(logits, target):
    p = softmax(logits)[target]
    return -math.log(p)

print(round(cross_entropy([5.0, 0.0, 0.0], 0), 4))   # 0.0135  confident & correct -> low loss
print(round(cross_entropy([5.0, 0.0, 0.0], 1), 4))   # 5.0136  confident & WRONG   -> high loss

Loss is -log of the probability assigned to the true class: near 0 when the model is confidently correct, large when it is confidently wrong. That asymmetry — punishing confident mistakes hardest — is why cross-entropy is the standard classification/LM training loss.

Exercise 3 · Scaled dot-product attentionAdvanced

Context: Scaled dot-product attention is the transformer core, and the detail interviewers probe is the 1/√d scaling — omit it and scores grow with dimension, saturating softmax and killing gradients.

Your task: Implement single-query scaled dot-product attention: score the query against keys, scale, softmax, then take the weighted sum of the values.

Requirements:

  • Score by dotting the query with each key
  • Divide the scores by √d before the softmax
  • Softmax the scaled scores into weights that sum to 1
  • Return the weighted sum of the value vectors (and optionally the weights)
  • Show the output attends most to the keys most aligned with the query

💡 Hint: Three steps — scaled scores, softmax weights, weighted sum — each a small comprehension; d is the query/key dimension.

Show solution

score → scale → softmax → weighted sum of values (pure stdlib):

import math

def softmax(xs):
    m = max(xs); e = [math.exp(x-m) for x in xs]; s = sum(e); return [v/s for v in e]

def attention(q, keys, values):
    d = len(q)
    scores = [sum(qi*ki for qi, ki in zip(q, k)) / math.sqrt(d) for k in keys]  # scale by 1/sqrt(d)
    weights = softmax(scores)
    dim = len(values[0])
    return [sum(w*v[j] for w, v in zip(weights, values)) for j in range(dim)], weights

q      = [1.0, 0.0]
keys   = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
values = [[10.0, 0.0], [0.0, 10.0], [5.0, 5.0]]
out, w = attention(q, keys, values)
print([round(x, 2) for x in out])         # attends most to key[0] and key[2]
print([round(x, 3) for x in w])           # attention weights sum to 1

The query dots each key to get relevance scores; dividing by sqrt(d) keeps scores from growing with dimension (which would saturate softmax); softmax turns them into weights that mix the values. That scaling factor is the detail interviewers probe — omit it and gradients vanish at large d.

Exercise 4 · Temperature & top-p samplingExpert

Context: Temperature and nucleus (top-p) sampling are the decoding knobs that control how an LLM picks tokens, and interviewers want you to explain what each does. They compose: temperature reshapes the distribution, then top-p truncates its tail.

Your task: Implement temperature scaling in softmax and a top_p nucleus filter on a probability distribution, and explain each knob.

Requirements:

  • Temperature < 1 sharpens toward the argmax; > 1 flattens toward uniform
  • Top-p keeps the smallest set of tokens whose cumulative mass reaches p
  • The kept 'nucleus' probabilities are renormalized to sum to 1
  • Low-mass tail tokens outside the nucleus are dropped
  • Show a sharpened distribution and a top-p filtered one on the same logits

💡 Hint: Divide logits by the temperature before softmax; for top-p, sort by probability descending and accumulate until the running mass crosses p.

Show solution

Temperature reshapes the distribution; top-p truncates its tail (runnable):

import math

def softmax(logits, temp=1.0):
    z = [x/temp for x in logits]          # temp<1 sharpens, temp>1 flattens
    m = max(z); e = [math.exp(x-m) for x in z]; s = sum(e); return [v/s for v in e]

def top_p(probs, p=0.9):
    ranked = sorted(enumerate(probs), key=lambda t: t[1], reverse=True)
    kept, cum = [], 0.0
    for idx, pr in ranked:
        kept.append((idx, pr)); cum += pr
        if cum >= p: break                # smallest set whose mass >= p (the "nucleus")
    z = sum(pr for _, pr in kept)
    return {idx: pr/z for idx, pr in kept}  # renormalize over the nucleus

logits = [3.0, 2.0, 1.0, 0.1]
print([round(x,3) for x in softmax(logits, temp=0.5)])  # sharper
print(top_p(softmax(logits), p=0.9))                    # drops the low-mass tail

Temperature <1 sharpens toward the argmax (more deterministic), >1 flattens (more random). Top-p keeps only the smallest set of tokens whose probability mass reaches p and renormalizes, cutting the unreliable tail. They compose: temperature reshapes, then top-p truncates.

Exercise 5 · Design a RAG system (A7 system design)Professional

Context: 'Design a RAG system' is a staple system-design interview, and a defensible answer names the pipeline and the two ways it breaks — a retrieval miss versus a hallucination — because they need different metrics and different fixes.

Your task: Produce the component design (ingestion → retrieval → generation → eval) and name the two failure modes you'd be graded on.

Requirements:

  • Ingestion: chunk (with situating context) → embed → store in a vector DB (ANN like HNSW/IVF at scale)
  • Retrieval: embed query → ANN top-k → hybrid + RRF → cross-encoder rerank → precise top-k
  • Generation: system prompt + retrieved context + question, with source citations
  • Eval: context recall/precision, faithfulness, answer relevance on a curated set, gated in CI
  • Name failure mode 1 (retrieval miss → low context recall) and its fix
  • Name failure mode 2 (hallucination → low faithfulness) and its fix

💡 Hint: The grader is listening for the distinction: a missed chunk is a recall problem you fix in retrieval, a fetched-but-ignored chunk is a faithfulness problem you fix in grounding.

Show solution

A defensible RAG design names components and failure modes (worked design):

Ingestion:  chunk docs (with a situating context prefix per chunk) -> embed ->
            store vectors in a vector DB (HNSW/IVF for ANN at scale).
Retrieval:  embed query -> ANN top-k -> hybrid (dense + BM25) + RRF fusion ->
            cross-encoder rerank -> precise top-k into the prompt.
Generation: prompt = system + retrieved context + question; cite sources.
Eval:       context_recall / context_precision / faithfulness / answer_relevance
            on a curated eval set; gate changes in CI.

Failure mode 1: RETRIEVAL miss -> answer-bearing chunk never fetched
                (low context_recall). Fix chunking/embeddings/top-k, not the prompt.
Failure mode 2: HALLUCINATION -> answer not grounded in retrieved context
                (low faithfulness). Fix grounding: rerank, tighter prompt, cite.

The grader wants the pipeline (ingest → retrieve → generate → eval) and the two ways it breaks: a retrieval miss (the chunk was never fetched — a recall problem) versus a hallucination (the chunk was fetched but the model didn't ground on it — a faithfulness problem). Different metrics, different fixes.

Exercise 6 · Make 1000 LLM calls efficiently (A3 scale)Industry scenario

Context: The scale question — run 1000 independent LLM calls for the least cost and wall-clock — wants you to name the four levers and show they multiply, not just 'call it in a loop'.

Your task: Write a decision function for 1000 calls that combines batching, caching, concurrency, and model choice, and quantifies the resulting cost.

Requirements:

  • Use the Batches API (≈50% off) when latency doesn't matter; concurrency when it does
  • Prompt-cache a shared prefix so repeated tokens cost a fraction
  • Pick the cheapest model that clears the quality bar
  • Compute a rough per-call cost from input/output tokens and multiply out to 1000
  • Show the levers stack (multiply) rather than being either/or

💡 Hint: Model each lever as a multiplier or an added step; start from a per-call token cost, then apply the batch/cache discounts to get the fleet total.

Show solution

Combine the four levers and quantify the win (runnable):

def plan_1000_calls(latency_sensitive, shared_prefix, in_tok, out_tok):
    steps, mult = [], 1.0
    if not latency_sensitive:
        steps.append("Batches API — 50%% off, async"); mult *= 0.5
    else:
        steps.append("bounded concurrency (async) — overlap the 1000 calls")
    if shared_prefix:
        steps.append("prompt-cache the shared prefix — repeated tokens ~0.1x")
    steps.append("cheapest model that clears the quality bar (Haiku for simple)")
    # rough per-call cost on Haiku ($1/$5 per MTok) before caching/batch:
    base = in_tok/1e6*1.0 + out_tok/1e6*5.0
    return steps, round(base * 1000 * mult, 2)

steps, cost = plan_1000_calls(False, True, in_tok=800, out_tok=200)
for s in steps: print("-", s)
print(f"~est batch cost for 1000 calls: ${cost}")

The four levers stack: batch for 50% off when latency doesn't matter, cache the shared prefix (~0.1x on repeated tokens), concurrency to overlap calls when it does matter, and pick the cheapest model that clears the bar. The grader wants you to name all four and show they multiply, not just "call it in a loop."

Knowledge check check yourself

✓ Knowledge check

For "implement scaled dot-product attention," the lesson gives softmax(QKᵀ/√d)·V. What is the role of the √d scaling, and what does the optional mask do?

Show answer
Dividing by the square root of the key dimension keeps the dot-product scores from growing large as dimension grows, keeping softmax out of its saturated region (stable gradients); the causal mask sets disallowed/future positions to -inf so softmax drives their weight to ~0.
✓ Knowledge check

The "make 1000 LLM calls efficiently" answer pairs a semaphore with retry/backoff on 429s. Why does the lesson insist you name both to earn the check-mark?

Show answer
The semaphore bounds concurrency (how many run at once), but you still hit transient rate-limit rejections; retry with exponential backoff on HTTP 429 handles those — concurrency capping and rate-limit recovery are two different controls a production fan-out needs together.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in