Implement decoder-only GPT-style transformer
Company: Amazon
Role: Machine Learning Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: This question evaluates implementation-level mastery of transformer architectures, including multi-head masked self-attention, position-wise feed-forward networks, residual connections, and precise tensor-shape reasoning for a decoder-only language model, within the Coding & Algorithms domain of deep learning and neural network engineering.
Part 1: Causal Multi-Head Self-Attention Forward Pass
Constraints
- 0 <= B <= 10
- 0 <= T <= 50
- 1 <= d_model <= 64 when T > 0
- 1 <= num_heads <= d_model
- d_model is divisible by num_heads
- All projection matrices are d_model x d_model
- Input values are finite numbers
Examples
Input: ([], 1, [], [], [], [])
Expected Output: []
Explanation: Empty batch produces an empty output.
Input: ([[[0], [1]]], 1, [[1]], [[1]], [[1]], [[1]])
Expected Output: [[[0], [0.731059]]]
Explanation: The second token attends to values [0, 1] with softmax scores [0, 1].
Input: ([[[1, 3], [2, 5], [4, 7]]], 1, [[0, 0], [0, 0]], [[0, 0], [0, 0]], [[1, 0], [0, 1]], [[1, 0], [0, 1]])
Expected Output: [[[1, 3], [1.5, 4], [2.333333, 5]]]
Explanation: Zero query/key projections make the causal attention uniform over all allowed previous positions.
Input: ([[[1, 2, 10, 20], [3, 4, 30, 40]]], 2, [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
Expected Output: [[[1, 2, 10, 20], [2, 3, 20, 30]]]
Explanation: Two heads are split across dimensions [0,1] and [2,3]; both use the same causal uniform averaging here.
Hints
- After projection, head h uses dimensions h * d_head through (h + 1) * d_head - 1.
- For each query position t, compute scores only against key positions j <= t; future positions should not appear in the softmax denominator.
Part 2: Position-Wise Feed-Forward Network
Constraints
- 0 <= B <= 100
- 0 <= T <= 100
- 1 <= d_model <= 128 when T > 0
- 1 <= d_ff <= 512
- All numeric values are finite
- Use ReLU: ReLU(z) = max(0, z)
Examples
Input: ([[[1, 2]]], [[1, -1, 0], [0, 2, 1]], [0, 1, -3], [[1, 0], [0, 1], [1, 1]], [0, 0])
Expected Output: [[[1, 4]]]
Explanation: The hidden vector is ReLU([1, 4, -1]) = [1, 4, 0].
Input: ([[[2], [-1]]], [[2, -1]], [0, 3], [[1], [-2]], [5])
Expected Output: [[[7], [-3]]]
Explanation: Both sequence positions use the same MLP independently.
Input: ([[]], [[1]], [0], [[1]], [0])
Expected Output: [[]]
Explanation: A batch with an empty sequence returns an empty sequence.
Input: ([[[1, -1]], [[0, 3]]], [[1, 1], [-1, 1]], [0, -2], [[2, 1], [1, -1]], [1, 0])
Expected Output: [[[5, 2]], [[2, -1]]]
Explanation: Different batch elements are processed independently with shared weights.
Hints
- There is no mixing across sequence positions; process each vector x[b][t] independently.
- Treat each vector as a row vector: output[j] = sum_i hidden[i] * W2[i][j] + b2[j].
Part 3: Pre-Norm Transformer Decoder Layer
Constraints
- 0 <= B <= 10
- 0 <= T <= 50
- 1 <= d_model <= 64 when T > 0
- 1 <= d_ff <= 256
- d_model is divisible by num_heads
- LayerNorm gamma and beta each have length d_model
- Use causal masking inside self-attention
Examples
Input: ([[[1, 3]]], 1, [1, 1], [0, 0], [[0, 0], [0, 0]], [[0, 0], [0, 0]], [[1, 0], [0, 1]], [[1, 0], [0, 1]], [1, 1], [0, 0], [[1, 0], [0, 1]], [0, 0], [[1, 0], [0, 1]], [0, 0])
Expected Output: [[[0, 5]]]
Explanation: LN([1,3]) = [-1,1], attention adds [-1,1], then the feed-forward residual adds [0,1].
Input: ([[[1, 3], [3, 1]]], 1, [1, 1], [0, 0], [[0, 0], [0, 0]], [[0, 0], [0, 0]], [[1, 0], [0, 1]], [[1, 0], [0, 1]], [1, 1], [0, 0], [[0, 0], [0, 0]], [0, 0], [[0, 0], [0, 0]], [0, 0])
Expected Output: [[[0, 4], [3, 1]]]
Explanation: The second position causally averages normalized values from positions 0 and 1; the feed-forward block is zero.
Input: ([[[5]]], 1, [1], [0], [[0]], [[0]], [[0]], [[0]], [1], [0], [[10]], [2], [[3]], [1])
Expected Output: [[[12]]]
Explanation: For a single feature, LayerNorm variance is zero, so the normalized vector is [0].
Input: ([], 1, [1], [0], [[1]], [[1]], [[1]], [[1]], [1], [0], [[1]], [0], [[1]], [0])
Expected Output: []
Explanation: Empty input produces empty output.
Hints
- This is a pre-norm block: normalize before attention and before the feed-forward network, not after the residual.
- Keep the original x for the first residual: x1 = x + attention(LN1(x)), then output = x1 + feedforward(LN2(x1)).
Part 4: Full GPT-Style Decoder-Only Forward Pass
Constraints
- 0 <= B <= 10
- 0 <= T <= 50
- 1 <= vocabulary size V <= 500
- 1 <= d_model <= 64 when T > 0
- 0 <= N <= 12
- d_model is divisible by num_heads
- Every input id is in the range [0, V - 1]
- All numeric parameters are finite
Examples
Input: ([[]], [[1, 2], [3, 4]], [], 1, [], [], [], [], [], [], [], [], [], [], [], [], [[1, 0], [0, 1]], [0, 0])
Expected Output: [[]]
Explanation: An empty sequence has no logits.
Input: ([[1]], [[1, 0], [0, 2]], [[10, 10]], 1, [], [], [], [], [], [], [], [], [], [], [], [], [[1, 0, 1], [0, 1, -1]], [0, 1, 0])
Expected Output: [[[10, 13, -2]]]
Explanation: With no decoder layers, logits are computed directly from token plus positional embedding.
Input: ([[0, 1], [1, 0]], [[1, 0], [0, 1]], [[0, 0], [10, 10]], 1, [], [], [], [], [], [], [], [], [], [], [], [], [[1, 0], [0, 1]], [0, 0])
Expected Output: [[[1, 0], [10, 11]], [[0, 1], [11, 10]]]
Explanation: The same positional embedding for index 1 is added in both batch elements.
Input: ([[0]], [[1, 3]], [[0, 0]], 1, [[1, 1]], [[0, 0]], [[[0, 0], [0, 0]]], [[[0, 0], [0, 0]]], [[[1, 0], [0, 1]]], [[[1, 0], [0, 1]]], [[1, 1]], [[0, 0]], [[[1, 0], [0, 1]]], [[0, 0]], [[[1, 0], [0, 1]]], [[0, 0]], [[1, 0], [0, 1]], [0, 0])
Expected Output: [[[0, 5]]]
Explanation: One decoder layer transforms the single hidden vector before the final projection.
Input: ([[0, 1]], [[1, 3], [3, 1]], [[0, 0], [0, 0]], 1, [[1, 1]], [[0, 0]], [[[0, 0], [0, 0]]], [[[0, 0], [0, 0]]], [[[1, 0], [0, 1]]], [[[1, 0], [0, 1]]], [[1, 1]], [[0, 0]], [[[0, 0], [0, 0]]], [[0, 0]], [[[0, 0], [0, 0]]], [[0, 0]], [[1, 0], [0, 1]], [0, 0])
Expected Output: [[[0, 4], [3, 1]]]
Explanation: The decoder layer uses causal attention, so position 0 cannot depend on position 1.
Hints
- The initial hidden state is token_embedding[input_ids[b][t]] + position_embedding[t].
- Apply decoder layers sequentially; the output of layer i becomes the input to layer i + 1.